diff --git a/.clue/id-ledger.yaml b/.clue/id-ledger.yaml new file mode 100644 index 0000000..efb1bc4 --- /dev/null +++ b/.clue/id-ledger.yaml @@ -0,0 +1,18 @@ +version: 2 +events: + - {id: C-001, kind: numeric, state: live, prefix: C, component: "1"} + - {id: VIS-001, kind: numeric, state: live, prefix: VIS, component: "1"} + - {id: CH-001, kind: numeric, state: reserved, prefix: CH, component: "1"} + - {id: G-001, kind: numeric, state: reserved, prefix: G, component: "1"} + - {id: CAP-001, kind: numeric, state: reserved, prefix: CAP, component: "1"} + - {id: ADR-001, kind: numeric, state: reserved, prefix: ADR, component: "1"} + - {id: CH-001, kind: numeric, state: live, prefix: CH, component: "1"} + - {id: CAP-001, kind: numeric, state: live, prefix: CAP, component: "1"} + - {id: G-001, kind: numeric, state: live, prefix: G, component: "1"} + - {id: ADR-001, kind: numeric, state: live, prefix: ADR, component: "1"} + - {id: CRIT-001, kind: numeric, state: reserved, prefix: CRIT, component: "1"} + - {id: DESIGN-001, kind: numeric, state: reserved, prefix: DESIGN, component: "1"} + - {id: CAP-002, kind: numeric, state: reserved, prefix: CAP, component: "2"} + - {id: CAP-003, kind: numeric, state: reserved, prefix: CAP, component: "3"} + - {id: CAP-002, kind: numeric, state: live, prefix: CAP, component: "2"} + - {id: CAP-003, kind: numeric, state: live, prefix: CAP, component: "3"} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..5d2c79b --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,73 @@ +name: Publish + +on: + push: + tags: ['v*.*.*'] + +permissions: + contents: read + packages: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Read the pinned Tank Royale commit + id: pin + shell: bash + run: echo "commit=$(tr -d '[:space:]' < TANK_ROYALE_COMMIT)" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@v4 + with: + repository: robocode-dev/tank-royale + ref: ${{ steps.pin.outputs.commit }} + path: tank-royale-source + persist-credentials: false + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '17' + - uses: actions/setup-python@v5 + with: + python-version: '3.14' + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - uses: actions/setup-node@v4 + with: + node-version: '24' + - name: Build four-language smoke bots + run: bash scripts/build-sample-bots.sh tank-royale-source + - uses: docker/setup-buildx-action@v3 + - name: Derive the image version from the tag + id: version + shell: bash + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + - name: Build and verify the hardened four-language container + env: + TANK_ROYALE_SOURCE: ${{ github.workspace }}/tank-royale-source + IMAGE_TAG: ghcr.io/robocode-dev/rumble-client:${{ steps.version.outputs.version }} + run: bash scripts/verify-container.sh + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Tag latest and push + env: + VERSION_TAG: ghcr.io/robocode-dev/rumble-client:${{ steps.version.outputs.version }} + run: | + docker tag "${VERSION_TAG}" ghcr.io/robocode-dev/rumble-client:latest + docker push "${VERSION_TAG}" + docker push ghcr.io/robocode-dev/rumble-client:latest + - name: Confirm the image is publicly pullable + env: + VERSION_TAG: ghcr.io/robocode-dev/rumble-client:${{ steps.version.outputs.version }} + run: | + docker logout ghcr.io + docker rmi "${VERSION_TAG}" ghcr.io/robocode-dev/rumble-client:latest + docker pull "${VERSION_TAG}" || { + echo "::error::${VERSION_TAG} is not publicly pullable. GitHub's API cannot set package visibility; a maintainer must set it to public once, by hand, in the package's GHCR settings (Package settings > Danger Zone > Change visibility), then re-run this workflow." + exit 1 + } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fa9d67b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Published container image: tagging a release now builds and pushes `ghcr.io/robocode-dev/rumble-client` so contributors can `docker pull` it instead of building it themselves. See `RELEASING.md` for the release steps. diff --git a/README.md b/README.md index cda915d..e1d814c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ For the complete newcomer-friendly walkthrough, including registration and token Docker is the recommended way to build and run the client: it supplies the complete Java, .NET, Python, and Node.js environment every ranked bot needs, and is the isolation boundary for running reviewed-but-untrusted bot code. Docker Engine or Docker Desktop is required for this path. 1. Clone this repository. -2. Build the image: `docker build --tag rumble-client:dev .` +2. Get the image: pull a released version and tag it locally — `docker pull ghcr.io/robocode-dev/rumble-client:latest && docker tag ghcr.io/robocode-dev/rumble-client:latest rumble-client:dev` — or build it yourself with `docker build --tag rumble-client:dev .` 3. Copy `rumble-client.example.json` to `rumble-client.json` and edit it — see [Configuration](#configuration) below. Never commit the resulting file. 4. Check your settings: `docker/rumble.sh validate rumble-client.json` 5. Check the bundled runtimes: `docker/rumble.sh runtimes` @@ -59,9 +59,15 @@ Copy `rumble-client.example.json` to `rumble-client.json`. Ranked mode requires Use one game type per configuration with the current command-line client. `--run` executes one battle using the first configured game type in contract-name order. `myBots` may list the names of active bots or teams owned by you, without version numbers; under-sampled matchups involving those entries receive priority. `battlesPerSession` is validated for the session contract, but the current one-battle command does not consume it. -## Docker and Podman development image +## Docker and Podman container image -The non-published development image can be built and run with Docker Engine, Docker Desktop, or Podman. The examples below use Docker; replace `docker` with `podman` when invoking the image directly. On Windows, Podman Desktop needs a running Linux virtual machine and can use WSL2 or Hyper-V as the provider; choose the provider when creating the machine. Podman Desktop/WSL2 on Windows and rootless Podman on Linux have both been manually verified for this image; neither is part of CI. +Released versions are published to `ghcr.io/robocode-dev/rumble-client`; pull one instead of building it yourself: + +```shell +docker pull ghcr.io/robocode-dev/rumble-client:latest +``` + +Substitute a specific version, e.g. `ghcr.io/robocode-dev/rumble-client:0.2.0`, to pin to a release; see [`CHANGELOG.md`](CHANGELOG.md) for what changed in each one. The image can also be built and run locally with Docker Engine, Docker Desktop, or Podman. The examples below use Docker; replace `docker` with `podman` when invoking the image directly. On Windows, Podman Desktop needs a running Linux virtual machine and can use WSL2 or Hyper-V as the provider; choose the provider when creating the machine. Podman Desktop/WSL2 on Windows and rootless Podman on Linux have both been manually verified for this image; neither is part of CI. Two flag differences from Docker are handled for you by the launcher scripts and do not need manual workarounds: diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..cac2744 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,12 @@ +# Releasing + +Releases are manual: a maintainer decides when to cut one, there is no release bot. Cutting a release publishes a new container image to `ghcr.io/robocode-dev/rumble-client` (see [CAP-001](docs/capabilities/CAP-001-published-container-image/README.md)). + +1. On `main`, move `CHANGELOG.md`'s `## [Unreleased]` heading to a dated version heading, e.g. `## [0.2.0] - 2026-09-13`, and start a fresh empty `## [Unreleased]` section above it. +2. Set the plain version in `gradle.properties` (drop the `-SNAPSHOT` suffix), e.g. `version=0.2.0`. +3. Commit both changes. +4. Tag the commit and push the tag: `git tag v0.2.0 && git push origin v0.2.0`. Pushing the tag triggers `.github/workflows/publish.yml`, which builds the `Dockerfile`, pushes `ghcr.io/robocode-dev/rumble-client:0.2.0` and `:latest`, then confirms the image is pullable without credentials. **On the first release**, this confirmation step is expected to fail: GitHub's API has no way to set a package's visibility, so a package `GITHUB_TOKEN` creates starts private. When it fails, go to the package's GHCR settings (Package settings → Danger Zone → Change visibility) and set it to public — a one-time step, since GitHub packages generally cannot be made private again — then re-run the failed workflow run. Every release after that should pass without intervention. +5. Bump `gradle.properties` back to the next `-SNAPSHOT` version, e.g. `version=0.3.0-SNAPSHOT`, and commit. +6. Create a GitHub Release for the tag referencing the `CHANGELOG.md` entry. + +Pushing a tag is the only thing that publishes an image; ordinary commits to `main` never do. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 919a358..a2784d6 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -9,6 +9,7 @@ The Rumble Client is a Java 17 command-line application packaged and run through - The Battle Runner executes the selected bots and produces battle and replay output; it is consumed as a Gradle dependency and is built from a local Tank Royale checkout while its pinned version is unreleased. - GitHub Issues provide the result inbox and receipt comments used by submission reconciliation. - Docker or Podman supplies the isolated multi-runtime environment for reviewed bot code; the host launcher scripts enforce read-only mounts, dropped capabilities, and resource limits. +- GHCR (`ghcr.io/robocode-dev/rumble-client`) distributes the built image on tagged releases, so contributors need not build it themselves. ## Components @@ -36,7 +37,7 @@ flowchart LR Submit --> Issues[GitHub Issues result inbox] ``` -Architecture documents describe the system's shape, not individual feature details. Durable choices include Java 17, Gradle, Gson for JSON contracts, the Tank Royale Battle Runner dependency, and Docker/Podman isolation for runtime execution. +Architecture documents describe the system's shape, not individual feature details. Durable choices include Java 17, Gradle, Gson for JSON contracts, the Tank Royale Battle Runner dependency, Docker/Podman isolation for runtime execution, and GHCR as the published-image distribution point (see [ADR-001](../decisions/ADR-001-ghcr-image-publishing-on-tag.md)). diff --git a/docs/capabilities/CAP-001-published-container-image/README.md b/docs/capabilities/CAP-001-published-container-image/README.md new file mode 100644 index 0000000..02306f7 --- /dev/null +++ b/docs/capabilities/CAP-001-published-container-image/README.md @@ -0,0 +1,18 @@ +--- +id: CAP-001 +type: capability +status: draft +provenance: inferred +reversal-cost: low +links: [G-001] +goal: G-001 +title: Published container image +--- + +# CAP-001 — Published container image + +The system publishes a versioned, pullable container image so a contributor can run ranked battles without building the four-language image themselves. Publishing happens once per human-authored release: a maintainer updates `CHANGELOG.md` and the Gradle version, pushes a `vX.Y.Z` tag, and CI builds the existing `Dockerfile` and pushes it to GHCR under that version and `latest`. + +This capability does not change how the image is built (the `Dockerfile` and `scripts/verify-container.sh` verification are unchanged) or how it runs (`docker/rumble.sh` / `rumble.ps1` are unchanged); it only adds a distribution path so `docker build` is no longer the only way to obtain the image. Local building remains supported for development, per the README's "Building `rumble-client` itself" section. + +See `criteria.md` for acceptance criteria and `design.md` for how the publish workflow fits together. diff --git a/docs/capabilities/CAP-001-published-container-image/criteria.md b/docs/capabilities/CAP-001-published-container-image/criteria.md new file mode 100644 index 0000000..51471ee --- /dev/null +++ b/docs/capabilities/CAP-001-published-container-image/criteria.md @@ -0,0 +1,33 @@ +--- +id: CAP-002 +type: criteria +status: draft +links: [CAP-001] +title: Acceptance criteria for the published container image +--- + +# Acceptance criteria — CAP-001 Published container image + +```gherkin +@AC-001 +Scenario: Tagging a release publishes a pullable versioned image + Test-type: Human + Given a maintainer has updated CHANGELOG.md and gradle.properties for version X.Y.Z on main + When they create and push the git tag "vX.Y.Z" + Then the publish workflow builds the repository's Dockerfile and pushes the image to ghcr.io/robocode-dev/rumble-client tagged "X.Y.Z" and "latest" + And a contributor can run "docker pull ghcr.io/robocode-dev/rumble-client:X.Y.Z" and use it in place of a locally built image + +@AC-002 +Scenario: Ordinary commits to main never publish an image + Test-type: Human + Given no version tag has been pushed + When a commit lands on main + Then no image is pushed to ghcr.io/robocode-dev/rumble-client + +@AC-003 +Scenario: Every published version has a changelog entry + Test-type: Human + Given a version tag "vX.Y.Z" has been pushed + When a contributor reads CHANGELOG.md + Then it contains a "## [X.Y.Z]" section describing what changed since the previous version +``` diff --git a/docs/capabilities/CAP-001-published-container-image/design.md b/docs/capabilities/CAP-001-published-container-image/design.md new file mode 100644 index 0000000..3c712bc --- /dev/null +++ b/docs/capabilities/CAP-001-published-container-image/design.md @@ -0,0 +1,28 @@ +--- +id: CAP-003 +type: design +status: draft +links: [CAP-001] +title: How the published container image works +--- + +# Design — CAP-001 Published container image + +```mermaid +flowchart LR + Maintainer -->|update CHANGELOG.md and gradle.properties, git tag vX.Y.Z| Tag[Tag pushed] + Tag --> Publish[publish.yml workflow] + Publish -->|docker build, --build-arg TANK_ROYALE_COMMIT| Image[Image built] + Image -->|scripts/verify-container.sh| Verify[Smoke-verified] + Verify -->|docker push| GHCR[ghcr.io/robocode-dev/rumble-client] + GHCR -->|logout, then anonymous docker pull| Confirm{Publicly pullable?} + Confirm -->|yes| Contributor + Confirm -->|no, first release only| Manual[Maintainer sets visibility public in GHCR settings] + Manual --> Confirm +``` + +`.github/workflows/publish.yml` triggers only on `push: tags: ['v*.*.*']`, separate from `build.yml`'s existing `docker` job (which builds and smoke-verifies the image on every push/PR but never pushes it anywhere). Both jobs build the same root `Dockerfile` with the same `TANK_ROYALE_COMMIT` build argument read from the `TANK_ROYALE_COMMIT` file, and both run `scripts/verify-container.sh` before the image is trusted; `publish.yml` additionally logs into `ghcr.io` with the workflow's `GITHUB_TOKEN` and pushes the verified image as `ghcr.io/robocode-dev/rumble-client:` and `:latest`. + +A package newly created on GHCR via `GITHUB_TOKEN` is not guaranteed to be public, which would silently defeat this capability's purpose (nobody could `docker pull` it without credentials) — and GitHub's API has no operation to set a package's visibility, so the workflow cannot fix this itself. Instead, after pushing, it proves the image is actually usable by logging out, removing the local image, and pulling it back down anonymously; a failure there fails the workflow run loudly rather than leaving a package a contributor cannot use. On the first release this is expected to fail once — `RELEASING.md` documents setting the package to public by hand in its GHCR settings, a one-time step, after which the same check should keep passing on every later release. + +Versioning is manual (see the repository's `RELEASING.md`): a maintainer edits `CHANGELOG.md`'s `## [Unreleased]` section into a dated version heading, sets the plain (non-`-SNAPSHOT`) version in `gradle.properties`, commits, and pushes a matching `vX.Y.Z` tag. The tag is what the workflow reacts to; `gradle.properties` is not read by CI to decide what to publish. diff --git a/docs/capabilities/README.md b/docs/capabilities/README.md index bbe6779..9c6c541 100644 --- a/docs/capabilities/README.md +++ b/docs/capabilities/README.md @@ -11,4 +11,5 @@ Each capability folder holds three files, updated together in the same change th A whole capability whose extracted criteria are not ready may stay `status: draft` with the gap stated, but a single unfinished criterion uses `@draft` without deactivating proven siblings — honesty at the narrowest level over artificial green. +- [CAP-001-published-container-image/](CAP-001-published-container-image/README.md) diff --git a/docs/decisions/ADR-001-ghcr-image-publishing-on-tag.md b/docs/decisions/ADR-001-ghcr-image-publishing-on-tag.md new file mode 100644 index 0000000..fa3a7a5 --- /dev/null +++ b/docs/decisions/ADR-001-ghcr-image-publishing-on-tag.md @@ -0,0 +1,17 @@ +--- +id: ADR-001 +type: decision +status: inferred +author: agent +accepted-by: [] +links: [CAP-001] +title: Publish the container image to GHCR, triggered only by version tags +--- + +# ADR-001 — Publish the container image to GHCR, triggered only by version tags + +The container image (CAP-001) is published to GitHub Container Registry (`ghcr.io/robocode-dev/rumble-client`) rather than Docker Hub or another registry, because it authenticates with the workflow's own `GITHUB_TOKEN` — no separate account, org, or long-lived secret to provision or rotate — and keeps the published artifact next to its source repository. + +Publishing is triggered only by pushing a `vX.Y.Z` tag, never by ordinary pushes to `main`. This keeps every image in the registry mapped to a real, changelogged version and keeps unstable/untagged commits out of the public registry; it also keeps `build.yml`'s existing per-push/PR `docker` job (build and smoke-verify only) unchanged, since publishing is a separate workflow that only runs on the tag event. + +Versioning and the changelog stay manual (no release-bot/Conventional-Commits automation): a human decides when a version is cut, edits `CHANGELOG.md` and `gradle.properties`, and pushes the tag. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 8618164..937c531 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -21,4 +21,5 @@ Decision records are timeless: state what is decided and only the enduring conte A decision that changes a methodology contract inventories every live carrier that states the affected contract and updates that complete inventory in the same change. Live carriers include current corpus truth, canonical and generated skills, templates, public or contributor guidance, implementation explanations, CLI text, and distribution metadata. Historical analyses, completed plans, and changelog entries remain pinned history. Focused guards hold stable repaired claims, but no current mechanism derives an arbitrary contract's complete carrier set, so the general obligation remains agent-enforced. +- [ADR-001 — Publish the container image to GHCR, triggered only by version tags](ADR-001-ghcr-image-publishing-on-tag.md) · `inferred` — The container image (CAP-001) is published to GitHub Container Registry (`ghcr.io/robocode-dev/rumble-client`) rather than Docker Hub or another registry, because it authenticates with the workflow's… diff --git a/docs/goals/G-001-obtain-image-without-building-it.md b/docs/goals/G-001-obtain-image-without-building-it.md new file mode 100644 index 0000000..3509efc --- /dev/null +++ b/docs/goals/G-001-obtain-image-without-building-it.md @@ -0,0 +1,15 @@ +--- +id: G-001 +type: goal +status: proposed +provenance: inferred +reversal-cost: low +links: [VIS-001] +title: Contributors obtain a ready-to-run container image without building it themselves +--- + +# G-001 — Contributors obtain a ready-to-run container image without building it themselves + +Contributors who want to run ranked battles want the isolated multi-runtime container from the Quickstart, not a build toolchain. Today the README calls the image "the non-published development image" and step 2 of the Quickstart is `docker build --tag rumble-client:dev .` — every contributor pays the four-language build cost (Gradle, a Tank Royale source checkout, Python wheel build, apt package install) before they can run a single battle. + +Publishing a versioned image that a contributor can `docker pull` removes that cost from the common path and gives a release a single, referenceable artifact. Local building remains available for development and for contributors changing the client itself. diff --git a/docs/goals/README.md b/docs/goals/README.md index 8f93ff4..3ba2f69 100644 --- a/docs/goals/README.md +++ b/docs/goals/README.md @@ -5,4 +5,5 @@ G-xxx: who wants the system and why — the reason anything else in this corpus A goal artifact answers three questions: who wants it, what they want, and why it matters. New wishes enter as `status: proposed` — the proposed goals **are** the inbox; a human promotes a goal to `accepted` when the project commits to it. Retiring a goal means deleting its file and naming its ID in a successor's `supersedes:` field. Plans link the goals they serve, so every milestone traces back to someone who wanted it. +- [G-001 — Contributors obtain a ready-to-run container image without building it themselves](G-001-obtain-image-without-building-it.md) · `proposed` — Contributors who want to run ranked battles want the isolated multi-runtime container from the Quickstart, not a build toolchain.