From 3a9408ed4cc324888e77cb2c6d4f1fc8ecae6de2 Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Sun, 13 Sep 2026 00:35:15 +0200 Subject: [PATCH 1/5] propose(CH-001): publish container image to GHCR on tagged releases Seeds the identity ledger (MIG-008) and opens CH-001, proposing to publish a versioned container image so contributors no longer have to build it themselves before running a battle. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Hma5K774H256sXrcqshaff --- .clue/id-ledger.yaml | 8 +++++++ .../open-questions.md | 11 ++++++++++ .../proposal.md | 22 +++++++++++++++++++ .../CH-001-publish-container-image/tasks.md | 17 ++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 .clue/id-ledger.yaml create mode 100644 changes/CH-001-publish-container-image/open-questions.md create mode 100644 changes/CH-001-publish-container-image/proposal.md create mode 100644 changes/CH-001-publish-container-image/tasks.md diff --git a/.clue/id-ledger.yaml b/.clue/id-ledger.yaml new file mode 100644 index 0000000..7902868 --- /dev/null +++ b/.clue/id-ledger.yaml @@ -0,0 +1,8 @@ +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"} diff --git a/changes/CH-001-publish-container-image/open-questions.md b/changes/CH-001-publish-container-image/open-questions.md new file mode 100644 index 0000000..cfe5140 --- /dev/null +++ b/changes/CH-001-publish-container-image/open-questions.md @@ -0,0 +1,11 @@ +--- +id: CH-001-open-questions +type: open-questions +status: resolved +links: [CH-001] +title: Open questions for CH-001 +--- + +# Open questions — CH-001 + +None outstanding. Registry (GHCR), versioning approach (manual), and publish trigger (tagged releases only) were confirmed with the user before implementation began. diff --git a/changes/CH-001-publish-container-image/proposal.md b/changes/CH-001-publish-container-image/proposal.md new file mode 100644 index 0000000..312acfa --- /dev/null +++ b/changes/CH-001-publish-container-image/proposal.md @@ -0,0 +1,22 @@ +--- +id: CH-001 +type: change +status: open +links: [] +title: Publish container image to GHCR on tagged releases +plan: none +--- + +# CH-001 — Publish container image to GHCR on tagged releases + +## What + +Add a capability (CAP-001) and the concrete mechanics for it: a `CHANGELOG.md`, a `RELEASING.md` describing the manual release steps, and a new `.github/workflows/publish.yml` that builds the existing `Dockerfile` and pushes it to `ghcr.io/robocode-dev/rumble-client` whenever a `vX.Y.Z` tag is pushed. Update the README's container section to stop calling the image "non-published" and add pull instructions. + +## Why + +The README currently tells every contributor to `docker build --tag rumble-client:dev .` before they can run a battle — there is no way to obtain a ready-built image. This change publishes one on each maintainer-cut release, without changing how the image is built, verified, or run. + +## Plan + +This change is plan-less: no `docs/plans/` entry exists yet for release/distribution work, and the user asked for this specific capability directly rather than as part of a larger campaign. diff --git a/changes/CH-001-publish-container-image/tasks.md b/changes/CH-001-publish-container-image/tasks.md new file mode 100644 index 0000000..1aa44a4 --- /dev/null +++ b/changes/CH-001-publish-container-image/tasks.md @@ -0,0 +1,17 @@ +--- +id: CH-001-tasks +type: tasks +status: open +links: [CH-001] +title: Tasks for CH-001 +--- + +# Tasks — CH-001 + +- [x] Add goal G-001 and capability CAP-001 (README, criteria.md with @AC-001/@AC-002/@AC-003, design.md) +- [x] Add ADR-001 recording the GHCR + tag-trigger + manual-versioning decision +- [ ] Add `CHANGELOG.md` (Keep a Changelog format, `## [Unreleased]` section) — serves AC-003 +- [ ] Add `RELEASING.md` describing the manual release steps (changelog, version bump, tag) +- [ ] Add `.github/workflows/publish.yml`, triggered on `push: tags: ['v*.*.*']` — serves AC-001, AC-002 +- [ ] Update `README.md` container section: drop "non-published" language, add `docker pull` instructions +- [ ] Update `docs/architecture/README.md` to name GHCR as the image distribution point From 52ec5b937bd3c89e869212f5169a715cc24abba5 Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Sun, 13 Sep 2026 00:36:35 +0200 Subject: [PATCH 2/5] implement(CH-001): publish container image to GHCR on tagged releases Adds CAP-001 (goal, criteria, design), ADR-001 for the GHCR/tag-trigger/ manual-versioning decision, CHANGELOG.md, RELEASING.md, the publish.yml workflow, and README/architecture updates. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Hma5K774H256sXrcqshaff --- .github/workflows/publish.yml | 61 +++++++++++++++++++ CHANGELOG.md | 11 ++++ README.md | 12 +++- RELEASING.md | 12 ++++ .../CH-001-publish-container-image/tasks.md | 10 +-- docs/architecture/README.md | 3 +- .../README.md | 17 ++++++ .../criteria.md | 31 ++++++++++ .../design.md | 22 +++++++ .../ADR-001-ghcr-image-publishing-on-tag.md | 17 ++++++ .../G-001-obtain-image-without-building-it.md | 15 +++++ 11 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 CHANGELOG.md create mode 100644 RELEASING.md create mode 100644 docs/capabilities/CAP-001-published-container-image/README.md create mode 100644 docs/capabilities/CAP-001-published-container-image/criteria.md create mode 100644 docs/capabilities/CAP-001-published-container-image/design.md create mode 100644 docs/decisions/ADR-001-ghcr-image-publishing-on-tag.md create mode 100644 docs/goals/G-001-obtain-image-without-building-it.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b14af48 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,61 @@ +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 + run: | + docker tag "ghcr.io/robocode-dev/rumble-client:${{ steps.version.outputs.version }}" ghcr.io/robocode-dev/rumble-client:latest + docker push "ghcr.io/robocode-dev/rumble-client:${{ steps.version.outputs.version }}" + docker push ghcr.io/robocode-dev/rumble-client:latest 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..e1854b9 --- /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` and pushes `ghcr.io/robocode-dev/rumble-client:0.2.0` and `:latest`. +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/changes/CH-001-publish-container-image/tasks.md b/changes/CH-001-publish-container-image/tasks.md index 1aa44a4..d7d7390 100644 --- a/changes/CH-001-publish-container-image/tasks.md +++ b/changes/CH-001-publish-container-image/tasks.md @@ -10,8 +10,8 @@ title: Tasks for CH-001 - [x] Add goal G-001 and capability CAP-001 (README, criteria.md with @AC-001/@AC-002/@AC-003, design.md) - [x] Add ADR-001 recording the GHCR + tag-trigger + manual-versioning decision -- [ ] Add `CHANGELOG.md` (Keep a Changelog format, `## [Unreleased]` section) — serves AC-003 -- [ ] Add `RELEASING.md` describing the manual release steps (changelog, version bump, tag) -- [ ] Add `.github/workflows/publish.yml`, triggered on `push: tags: ['v*.*.*']` — serves AC-001, AC-002 -- [ ] Update `README.md` container section: drop "non-published" language, add `docker pull` instructions -- [ ] Update `docs/architecture/README.md` to name GHCR as the image distribution point +- [x] Add `CHANGELOG.md` (Keep a Changelog format, `## [Unreleased]` section) — serves AC-003 +- [x] Add `RELEASING.md` describing the manual release steps (changelog, version bump, tag) +- [x] Add `.github/workflows/publish.yml`, triggered on `push: tags: ['v*.*.*']` — serves AC-001, AC-002 +- [x] Update `README.md` container section: drop "non-published" language, add `docker pull` instructions +- [x] Update `docs/architecture/README.md` to name GHCR as the image distribution point 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..12bb2ad --- /dev/null +++ b/docs/capabilities/CAP-001-published-container-image/README.md @@ -0,0 +1,17 @@ +--- +id: CAP-001 +type: capability +status: draft +provenance: inferred +reversal-cost: low +links: [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..3b394f3 --- /dev/null +++ b/docs/capabilities/CAP-001-published-container-image/criteria.md @@ -0,0 +1,31 @@ +--- +id: CAP-001-criteria +type: criteria +status: draft +links: [CAP-001] +title: Acceptance criteria for the published container image +--- + +# Acceptance criteria — CAP-001 Published container image + +@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..ac44754 --- /dev/null +++ b/docs/capabilities/CAP-001-published-container-image/design.md @@ -0,0 +1,22 @@ +--- +id: CAP-001-design +type: design +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 -->|docker pull| Contributor +``` + +`.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`. + +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/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/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. From dd86a67a3e0a2f890671c59bb25ea5d60edd48db Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Sun, 13 Sep 2026 00:45:11 +0200 Subject: [PATCH 3/5] digest(CH-001): fold container-image capability into the corpus Regenerates README indexes and deletes the CH-001 change workspace now that every task is done; docs/capabilities/CAP-001-published-container-image, docs/goals/G-001, and docs/decisions/ADR-001 carry the durable record. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Hma5K774H256sXrcqshaff --- .clue/id-ledger.yaml | 10 +++++++++ .../open-questions.md | 11 ---------- .../proposal.md | 22 ------------------- .../CH-001-publish-container-image/tasks.md | 17 -------------- .../README.md | 1 + .../criteria.md | 4 +++- .../design.md | 3 ++- docs/capabilities/README.md | 1 + docs/decisions/README.md | 1 + docs/goals/README.md | 1 + 10 files changed, 19 insertions(+), 52 deletions(-) delete mode 100644 changes/CH-001-publish-container-image/open-questions.md delete mode 100644 changes/CH-001-publish-container-image/proposal.md delete mode 100644 changes/CH-001-publish-container-image/tasks.md diff --git a/.clue/id-ledger.yaml b/.clue/id-ledger.yaml index 7902868..efb1bc4 100644 --- a/.clue/id-ledger.yaml +++ b/.clue/id-ledger.yaml @@ -6,3 +6,13 @@ events: - {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/changes/CH-001-publish-container-image/open-questions.md b/changes/CH-001-publish-container-image/open-questions.md deleted file mode 100644 index cfe5140..0000000 --- a/changes/CH-001-publish-container-image/open-questions.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: CH-001-open-questions -type: open-questions -status: resolved -links: [CH-001] -title: Open questions for CH-001 ---- - -# Open questions — CH-001 - -None outstanding. Registry (GHCR), versioning approach (manual), and publish trigger (tagged releases only) were confirmed with the user before implementation began. diff --git a/changes/CH-001-publish-container-image/proposal.md b/changes/CH-001-publish-container-image/proposal.md deleted file mode 100644 index 312acfa..0000000 --- a/changes/CH-001-publish-container-image/proposal.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: CH-001 -type: change -status: open -links: [] -title: Publish container image to GHCR on tagged releases -plan: none ---- - -# CH-001 — Publish container image to GHCR on tagged releases - -## What - -Add a capability (CAP-001) and the concrete mechanics for it: a `CHANGELOG.md`, a `RELEASING.md` describing the manual release steps, and a new `.github/workflows/publish.yml` that builds the existing `Dockerfile` and pushes it to `ghcr.io/robocode-dev/rumble-client` whenever a `vX.Y.Z` tag is pushed. Update the README's container section to stop calling the image "non-published" and add pull instructions. - -## Why - -The README currently tells every contributor to `docker build --tag rumble-client:dev .` before they can run a battle — there is no way to obtain a ready-built image. This change publishes one on each maintainer-cut release, without changing how the image is built, verified, or run. - -## Plan - -This change is plan-less: no `docs/plans/` entry exists yet for release/distribution work, and the user asked for this specific capability directly rather than as part of a larger campaign. diff --git a/changes/CH-001-publish-container-image/tasks.md b/changes/CH-001-publish-container-image/tasks.md deleted file mode 100644 index d7d7390..0000000 --- a/changes/CH-001-publish-container-image/tasks.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -id: CH-001-tasks -type: tasks -status: open -links: [CH-001] -title: Tasks for CH-001 ---- - -# Tasks — CH-001 - -- [x] Add goal G-001 and capability CAP-001 (README, criteria.md with @AC-001/@AC-002/@AC-003, design.md) -- [x] Add ADR-001 recording the GHCR + tag-trigger + manual-versioning decision -- [x] Add `CHANGELOG.md` (Keep a Changelog format, `## [Unreleased]` section) — serves AC-003 -- [x] Add `RELEASING.md` describing the manual release steps (changelog, version bump, tag) -- [x] Add `.github/workflows/publish.yml`, triggered on `push: tags: ['v*.*.*']` — serves AC-001, AC-002 -- [x] Update `README.md` container section: drop "non-published" language, add `docker pull` instructions -- [x] Update `docs/architecture/README.md` to name GHCR as the image distribution point diff --git a/docs/capabilities/CAP-001-published-container-image/README.md b/docs/capabilities/CAP-001-published-container-image/README.md index 12bb2ad..02306f7 100644 --- a/docs/capabilities/CAP-001-published-container-image/README.md +++ b/docs/capabilities/CAP-001-published-container-image/README.md @@ -5,6 +5,7 @@ status: draft provenance: inferred reversal-cost: low links: [G-001] +goal: G-001 title: Published container image --- diff --git a/docs/capabilities/CAP-001-published-container-image/criteria.md b/docs/capabilities/CAP-001-published-container-image/criteria.md index 3b394f3..51471ee 100644 --- a/docs/capabilities/CAP-001-published-container-image/criteria.md +++ b/docs/capabilities/CAP-001-published-container-image/criteria.md @@ -1,5 +1,5 @@ --- -id: CAP-001-criteria +id: CAP-002 type: criteria status: draft links: [CAP-001] @@ -8,6 +8,7 @@ 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 @@ -29,3 +30,4 @@ Scenario: Every published version has a changelog entry 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 index ac44754..d19c482 100644 --- a/docs/capabilities/CAP-001-published-container-image/design.md +++ b/docs/capabilities/CAP-001-published-container-image/design.md @@ -1,6 +1,7 @@ --- -id: CAP-001-design +id: CAP-003 type: design +status: draft links: [CAP-001] title: How the published container image works --- 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/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/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. From 9c57e308c48da2532ab6a73b3038fb7b4fb6b7b5 Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Sun, 13 Sep 2026 00:53:19 +0200 Subject: [PATCH 4/5] fix(CH-001): make the published GHCR package public and verify it Closes the review's advisory gap: a package GITHUB_TOKEN creates on GHCR is not guaranteed public, which would silently defeat AC-001. publish.yml now sets visibility to public after pushing and fails the run if an anonymous pull of the just-pushed tag doesn't work. Also moves the tag/push step's version interpolation into an env var instead of the run: script body. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Hma5K774H256sXrcqshaff --- .github/workflows/publish.yml | 19 +++++++++++++++++-- RELEASING.md | 2 +- .../design.md | 5 ++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b14af48..81e1264 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,7 +55,22 @@ jobs: 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 "ghcr.io/robocode-dev/rumble-client:${{ steps.version.outputs.version }}" ghcr.io/robocode-dev/rumble-client:latest - docker push "ghcr.io/robocode-dev/rumble-client:${{ steps.version.outputs.version }}" + 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: Set the GHCR package to public + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api --method PATCH /orgs/robocode-dev/packages/container/rumble-client -f visibility=public \ + || echo "::warning::Could not set ghcr.io/robocode-dev/rumble-client to public automatically; the next step will fail if it is still private. Set visibility to public in the package's GHCR settings." + - 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}" diff --git a/RELEASING.md b/RELEASING.md index e1854b9..50c86c3 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -5,7 +5,7 @@ Releases are manual: a maintainer decides when to cut one, there is no release b 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` and pushes `ghcr.io/robocode-dev/rumble-client:0.2.0` and `:latest`. +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`, sets the GHCR package to public, then confirms the image is pullable without credentials. If the workflow fails on that last confirmation step, the package's visibility couldn't be set automatically — set it to public by hand in the package's GHCR settings, then re-run the workflow. 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. diff --git a/docs/capabilities/CAP-001-published-container-image/design.md b/docs/capabilities/CAP-001-published-container-image/design.md index d19c482..baaa46a 100644 --- a/docs/capabilities/CAP-001-published-container-image/design.md +++ b/docs/capabilities/CAP-001-published-container-image/design.md @@ -15,9 +15,12 @@ flowchart LR 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 -->|docker pull| Contributor + GHCR -->|set visibility public, then anonymous docker pull| Confirm[Confirmed publicly pullable] + Confirm --> Contributor ``` `.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). After pushing, the workflow explicitly sets the package's visibility to public via the GitHub API, then proves the fix worked by logging out, removing the local image, and pulling it back down anonymously — a failure at that last step fails the workflow run rather than leaving a package a contributor cannot actually use. + 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. From c7df0c9ba6c4d79caf0fcc9bf4ee2b15a4d03e5d Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Sun, 13 Sep 2026 00:58:04 +0200 Subject: [PATCH 5/5] fix(CH-001): drop the nonexistent GHCR visibility API call Review found that GitHub's API has no operation to change a package's visibility, so the prior fix's "Set the GHCR package to public" step would 404 on every run. Remove it; the pull-confirmation step now fails loudly and names the real, one-time manual fix (GHCR package settings), and RELEASING.md/design.md describe that honestly instead of claiming automation that doesn't exist. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Hma5K774H256sXrcqshaff --- .github/workflows/publish.yml | 11 ++++------- RELEASING.md | 2 +- .../CAP-001-published-container-image/design.md | 8 +++++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 81e1264..5d2c79b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -61,16 +61,13 @@ jobs: 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: Set the GHCR package to public - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh api --method PATCH /orgs/robocode-dev/packages/container/rumble-client -f visibility=public \ - || echo "::warning::Could not set ghcr.io/robocode-dev/rumble-client to public automatically; the next step will fail if it is still private. Set visibility to public in the package's GHCR settings." - 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}" + 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/RELEASING.md b/RELEASING.md index 50c86c3..cac2744 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -5,7 +5,7 @@ Releases are manual: a maintainer decides when to cut one, there is no release b 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`, sets the GHCR package to public, then confirms the image is pullable without credentials. If the workflow fails on that last confirmation step, the package's visibility couldn't be set automatically — set it to public by hand in the package's GHCR settings, then re-run the workflow. +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. diff --git a/docs/capabilities/CAP-001-published-container-image/design.md b/docs/capabilities/CAP-001-published-container-image/design.md index baaa46a..3c712bc 100644 --- a/docs/capabilities/CAP-001-published-container-image/design.md +++ b/docs/capabilities/CAP-001-published-container-image/design.md @@ -15,12 +15,14 @@ flowchart LR 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 -->|set visibility public, then anonymous docker pull| Confirm[Confirmed publicly pullable] - Confirm --> Contributor + 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). After pushing, the workflow explicitly sets the package's visibility to public via the GitHub API, then proves the fix worked by logging out, removing the local image, and pulling it back down anonymously — a failure at that last step fails the workflow run rather than leaving a package a contributor cannot actually use. +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.