Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .clue/id-ledger.yaml
Original file line number Diff line number Diff line change
@@ -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"}
73 changes: 73 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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:

Expand Down
12 changes: 12 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion docs/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)).

<!-- clue:index:start -->
<!-- clue:index:end -->
18 changes: 18 additions & 0 deletions docs/capabilities/CAP-001-published-container-image/README.md
Original file line number Diff line number Diff line change
@@ -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.
33 changes: 33 additions & 0 deletions docs/capabilities/CAP-001-published-container-image/criteria.md
Original file line number Diff line number Diff line change
@@ -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
```
28 changes: 28 additions & 0 deletions docs/capabilities/CAP-001-published-container-image/design.md
Original file line number Diff line number Diff line change
@@ -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:<version>` 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.
1 change: 1 addition & 0 deletions docs/capabilities/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- clue:index:start -->
- [CAP-001-published-container-image/](CAP-001-published-container-image/README.md)
<!-- clue:index:end -->
17 changes: 17 additions & 0 deletions docs/decisions/ADR-001-ghcr-image-publishing-on-tag.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- clue:index:start -->
- [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…
<!-- clue:index:end -->
15 changes: 15 additions & 0 deletions docs/goals/G-001-obtain-image-without-building-it.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading