name: Release on: release: types: [published] workflow_dispatch: jobs: publish-images: name: Build & push all RoboCo images to GHCR + Docker Hub runs-on: ubuntu-latest permissions: contents: read packages: write outputs: version: ${{ steps.ver.outputs.version }} env: GHCR: ghcr.io/rennf93 # GitHub Container Registry namespace DOCKERHUB: docker.io/renzof93 # Docker Hub namespace (different username) steps: - name: Checkout code uses: actions/checkout@v7 - name: Free up runner disk space run: | # Eleven images on one runner is disk-heavy; drop preinstalled tooling # we don't use so the builds don't run out of space. sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL || true df -h / - name: Log in to GitHub Container Registry uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log in to Docker Hub uses: docker/login-action@v4 with: username: renzof93 password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Derive version tag id: ver run: | # release → the tag (v0.1.0 → 0.1.0). Any manual dispatch runs against a # branch whose name can contain "/" (e.g. feature/x) — not a valid image # tag — so use the short SHA, which always is one. if [ "${{ github.event_name }}" = "release" ]; then RAW="${{ github.event.release.tag_name }}" VERSION="${RAW#v}" else VERSION="$(git rev-parse --short HEAD)" fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Release version: $VERSION" - name: Build & push every RoboCo image env: VERSION: ${{ steps.ver.outputs.version }} run: | set -euo pipefail # Tag one built image for both registries at :VERSION and :latest. regtags() { local name="$1" echo "-t ${GHCR}/${name}:${VERSION} -t ${GHCR}/${name}:latest" \ "-t ${DOCKERHUB}/${name}:${VERSION} -t ${DOCKERHUB}/${name}:latest" } # Push one image's four tags (both registries, both labels). pushall() { local name="$1" for ref in "${GHCR}/${name}" "${DOCKERHUB}/${name}"; do docker push "${ref}:${VERSION}" docker push "${ref}:latest" done } # agent-base MUST build first: the agent images build # `FROM roboco-agent-base` — a local tag that has to exist in the daemon # before they build. Tag it locally (for the FROM) and for both registries. echo "::group::build roboco-agent-base" docker build -f docker/agent-base.Dockerfile \ -t roboco-agent-base $(regtags roboco-agent-base) . echo "::endgroup::" # roboco-agent-grok MUST build next, ahead of the loop: the two # interactive Grok roles (prompter, secretary) build `FROM # roboco-agent-grok` — a local tag that has to exist in the daemon # before they build, and bash associative-array iteration order is # unspecified, so it can't just be another entry in IMAGES below. echo "::group::build roboco-agent-grok" docker build -f docker/agent-grok.Dockerfile \ -t roboco-agent-grok $(regtags roboco-agent-grok) . echo "::endgroup::" # Every other image → its Dockerfile. Names mirror docker-compose's # `image:` values exactly, so compose can later pull instead of build. declare -A IMAGES=( [roboco-orchestrator]=docker/orchestrator.Dockerfile [roboco-panel]=docker/panel.Dockerfile [roboco-video-renderer]=docker/video-renderer.Dockerfile [roboco-sandbox-pg]=docker/sandbox-pg.Dockerfile [roboco-agent-pm]=docker/agent-pm.Dockerfile [roboco-agent-dev-be]=docker/agent-dev-be.Dockerfile [roboco-agent-dev-fe]=docker/agent-dev-fe.Dockerfile [roboco-agent-qa-be]=docker/agent-qa-be.Dockerfile [roboco-agent-qa-fe]=docker/agent-qa-fe.Dockerfile [roboco-agent-ux]=docker/agent-ux.Dockerfile [roboco-agent-doc]=docker/agent-doc.Dockerfile [roboco-agent-prompter]=docker/agent-prompter.Dockerfile [roboco-agent-secretary]=docker/agent-secretary.Dockerfile [roboco-agent-pr-reviewer]=docker/agent-pr-reviewer.Dockerfile [roboco-agent-grok-prompter]=docker/agent-grok-prompter.Dockerfile [roboco-agent-grok-secretary]=docker/agent-grok-secretary.Dockerfile [roboco-agent-codex]=docker/agent-codex.Dockerfile # Gemini (Google, official CLI) — one-shot delivery roles only (V1), # no interactive prompter/secretary variant depends FROM it, so # (unlike roboco-agent-grok above) it needs no special build-order # carve-out and is just another entry here. [roboco-agent-gemini]=docker/agent-gemini.Dockerfile ) for name in "${!IMAGES[@]}"; do echo "::group::build ${name}" docker build -f "${IMAGES[$name]}" $(regtags "${name}") . echo "::endgroup::" done # Push only after every build succeeds, so a failure never leaves a # half-published release. Base first, then the rest. echo "::group::push roboco-agent-base" pushall roboco-agent-base echo "::endgroup::" echo "::group::push roboco-agent-grok" pushall roboco-agent-grok echo "::endgroup::" for name in "${!IMAGES[@]}"; do echo "::group::push ${name}" pushall "${name}" echo "::endgroup::" done echo "Published roboco-agent-base + roboco-agent-grok + ${#IMAGES[@]} more images to GHCR + Docker Hub at :${VERSION} and :latest" pull-smoke: name: Registry pull smoke — verify every published image is pullable needs: publish-images runs-on: ubuntu-latest permissions: contents: read packages: read # This is the exact regression class the docs/CHANGELOG already record # once ("Release images complete" — the release workflow's build matrix # was missing the two Grok sub-images, so docker-compose.registry.yml's # pull failed for every fresh user until the next release). Running the # literal command a user runs, against the tag that was just published, # catches a missing/mistagged image before anyone else does. env: ROBOCO_REGISTRY: ghcr.io/rennf93 ROBOCO_VERSION: ${{ needs.publish-images.outputs.version }} # docker-compose.registry.yml hard-requires these three non-empty # (`${VAR:?...}` on the orchestrator/nginx services) just to # INTERPOLATE the file — `pull` never touches their runtime values, so # placeholders are enough to get past that gate (see scripts/bootstrap.sh # for what a real deploy generates here). ROBOCO_ENCRYPTION_KEY: pull-smoke-placeholder-not-a-real-key ROBOCO_AGENT_AUTH_SECRET: pull-smoke-placeholder-not-a-real-secret ROBOCO_PANEL_AGENT_TOKEN: pull-smoke-placeholder-not-a-real-token steps: - name: Checkout code uses: actions/checkout@v7 - name: Free up runner disk space run: | # Same trick as publish-images: pulling ~20 images (roboco's own # plus postgres/redis/ollama/nginx) on one runner is disk-heavy. sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL || true df -h / - name: Log in to GitHub Container Registry uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Pull every image docker-compose.registry.yml references run: | echo "Pulling ${ROBOCO_REGISTRY}/roboco-*:${ROBOCO_VERSION} (+ upstream deps) — failing on any missing image..." docker compose -f docker-compose.registry.yml pull