name: Publish Docker Image on: push: tags: - 'v*' # Serialize concurrent runs of the same workflow on the same ref so the # build jobs can't race `docker system prune` in the smoke gates # (pruning from one job can nuke another job's in-flight buildx cache). # cancel-in-progress: false — tag pushes are release events, we never # want to silently drop one. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false # Plain progress output from BuildKit — critical for diagnosing stalls # inside arm64-under-QEMU builds where the default collapsed progress UI # hides which step is stuck. env: BUILDKIT_PROGRESS: plain # Runner disk pressure notes: # Gitea Actions runners use `catthehacker/ubuntu:act-latest` on a shared host # with limited overlay space (~40 GB, often 70%+ used at start). Two jobs # per variant: # * smoke gate (amd64 only, `load: true` into local dockerd for smoke # testing) — peak disk = tarball + unpacked image + buildx cache. The # `Reclaim runner disk` step below strips catthehacker-resident # toolchains and prunes stale docker state before buildx starts. # * build job (amd64 + arm64, `push-by-digest` streaming directly to # Docker Hub, no local unpack). Peak disk on push-by-digest is # BuildKit's content store only — much smaller than `load: true`. # `docker/build-push-action@v7` with comma-separated platforms # publishes a proper multi-arch manifest in one step. # # Why not matrix + digest artifacts? # An earlier revision split each arch into its own matrix job and used # `actions/upload-artifact` to pass digests to a merge job. On Gitea # Actions, `actions/{upload,download}-artifact@v4+` fails with # `GHESNotSupportedError` — v4 relies on a GitHub-specific Artifact # API that Gitea doesn't implement. Rather than downgrade to @v3 (the # last Gitea-compatible release) we collapsed back to single-job # multi-arch push. The matrix only helps when the build literally # cannot fit on one runner, which push-by-digest + reclaim no longer # hits for this image. # # Gitea Actions gotchas baked into this file: # * `actions/{upload,download}-artifact` must stay at @v3 on Gitea. # * Step scripts run under /bin/sh (dash) — no bash-isms like # ${VAR//a/b}. Use `tr` or explicit `shell: bash`. # * `docker/build-push-action@v7` with `platforms: a,b` works for # multi-arch push natively; no matrix/merge dance needed. jobs: # ── Smoke test (amd64 only, gates the push jobs) ──────────────────── smoke-base: runs-on: ubuntu-latest container: image: catthehacker/ubuntu:act-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Force IPv4 for Docker Hub run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf # See docker-publish.yml preamble. `load: true` peak disk = tarball # + unpacked image + buildx cache; the image now crosses the 40 GB # runner overlay's starting headroom. Strip catthehacker-resident # toolchains and any stale docker state up front. - name: Reclaim runner disk run: | set -x df -h / || true rm -rf \ /opt/hostedtoolcache \ /opt/microsoft \ /opt/az \ /opt/ghc \ /usr/local/.ghcup \ /usr/share/dotnet \ /usr/share/swift \ /usr/local/lib/android \ /usr/local/share/powershell \ /usr/local/share/chromium \ /usr/local/share/boost \ /usr/lib/jvm 2>/dev/null || true apt-get clean || true rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true docker system df || true docker system prune -af --volumes || true docker builder prune -af || true df -h / || true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 with: driver-opts: network=host - name: Build and load amd64 image for smoke test uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64 push: false load: true tags: opencode-devbox:smoke-base - name: Smoke test (amd64) run: bash scripts/smoke-test.sh opencode-devbox:smoke-base --variant base smoke-omos: runs-on: ubuntu-latest container: image: catthehacker/ubuntu:act-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Force IPv4 for Docker Hub run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf - name: Reclaim runner disk run: | set -x df -h / || true rm -rf \ /opt/hostedtoolcache \ /opt/microsoft \ /opt/az \ /opt/ghc \ /usr/local/.ghcup \ /usr/share/dotnet \ /usr/share/swift \ /usr/local/lib/android \ /usr/local/share/powershell \ /usr/local/share/chromium \ /usr/local/share/boost \ /usr/lib/jvm 2>/dev/null || true apt-get clean || true rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true docker system df || true docker system prune -af --volumes || true docker builder prune -af || true df -h / || true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 with: driver-opts: network=host - name: Build and load amd64 image for smoke test uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64 push: false load: true build-args: | INSTALL_OMOS=true tags: opencode-devbox:smoke-omos - name: Smoke test (amd64) run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos # ── Multi-arch push (single job per variant, comma-separated platforms) ─ build-base: runs-on: ubuntu-latest needs: smoke-base timeout-minutes: 90 container: image: catthehacker/ubuntu:act-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Force IPv4 for Docker Hub run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf # Lighter reclaim than the smoke-gate version: push-by-digest # doesn't write to host dockerd, so `docker system prune` adds # little. BuildKit cache from prior runs is the thing to clear. - name: Reclaim runner disk run: | set -x df -h / || true rm -rf \ /opt/hostedtoolcache \ /opt/microsoft \ /opt/az \ /opt/ghc \ /usr/local/.ghcup \ /usr/share/dotnet \ /usr/share/swift \ /usr/local/lib/android \ /usr/local/share/powershell \ /usr/local/share/chromium \ /usr/local/share/boost \ /usr/lib/jvm 2>/dev/null || true apt-get clean || true rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true docker builder prune -af || true df -h / || true - name: Set up QEMU uses: docker/setup-qemu-action@v4 with: platforms: arm64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 with: driver-opts: network=host - name: Login to Docker Hub uses: docker/login-action@v4 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract version from tag id: version run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - name: Build and push (multi-arch) uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: | ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }} ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest build-omos: runs-on: ubuntu-latest needs: smoke-omos timeout-minutes: 90 container: image: catthehacker/ubuntu:act-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Force IPv4 for Docker Hub run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf - name: Reclaim runner disk run: | set -x df -h / || true rm -rf \ /opt/hostedtoolcache \ /opt/microsoft \ /opt/az \ /opt/ghc \ /usr/local/.ghcup \ /usr/share/dotnet \ /usr/share/swift \ /usr/local/lib/android \ /usr/local/share/powershell \ /usr/local/share/chromium \ /usr/local/share/boost \ /usr/lib/jvm 2>/dev/null || true apt-get clean || true rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true docker builder prune -af || true df -h / || true - name: Set up QEMU uses: docker/setup-qemu-action@v4 with: platforms: arm64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 with: driver-opts: network=host - name: Login to Docker Hub uses: docker/login-action@v4 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract version from tag id: version run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - name: Build and push (multi-arch) uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64 push: true build-args: | INSTALL_OMOS=true tags: | ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-omos update-description: runs-on: ubuntu-latest needs: [build-base, build-omos] container: image: catthehacker/ubuntu:act-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Update Docker Hub description run: | TOKEN=$(curl -s -X POST https://hub.docker.com/v2/auth/token \ -H "Content-Type: application/json" \ -d '{"identifier":"${{ vars.DOCKERHUB_USERNAME }}","secret":"${{ secrets.DOCKERHUB_TOKEN }}"}' \ | jq -r .access_token) if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ]; then echo "::error::Failed to authenticate with Docker Hub API" exit 1 fi HTTP_CODE=$(jq -n \ --rawfile full DOCKER_HUB.md \ --arg short "Portable AI dev environment for opencode. Debian-based with git, Node.js, AWS CLI, and SSH support." \ '{"full_description": $full, "description": $short}' | \ curl -s -o /tmp/hub-response.txt -w "%{http_code}" -X PATCH \ "https://hub.docker.com/v2/repositories/${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox/" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @-) echo "Docker Hub API returned: $HTTP_CODE" if [ "$HTTP_CODE" != "200" ]; then echo "Response body:" cat /tmp/hub-response.txt echo "::error::Docker Hub description update failed with HTTP $HTTP_CODE" exit 1 fi