Files
opencode-devbox/.gitea/workflows/docker-publish.yml
T
Joakim Persson fc74a8f906
Validate / docs-check (push) Successful in 17s
Validate / validate-omos (push) Successful in 14m21s
Validate / validate-base (push) Successful in 14m50s
Publish Docker Image / smoke-base (push) Successful in 11m12s
Publish Docker Image / smoke-omos (push) Successful in 22m0s
Publish Docker Image / build-base (push) Successful in 42m25s
Publish Docker Image / build-omos (push) Failing after 1h16m24s
Publish Docker Image / update-description (push) Has been cancelled
Collapse per-arch matrix back into single multi-arch push jobs
v1.14.31c's matrix jobs failed on Upload digest with GHESNotSupportedError
— Gitea Actions doesn't support actions/upload-artifact@v4+.
Separately, build-omos arm64 hung silently for 12 min in Set-up job,
likely catthehacker pull contention between concurrent matrix children.

Rather than downgrade artifacts to @v3, collapse the matrix entirely.
docker/build-push-action@v7 with platforms: linux/amd64,linux/arm64
publishes a proper multi-arch manifest in one job, so the
artifact-passing and imagetools create merge dance only existed to
support a matrix split we no longer need.

The matrix was designed around load: true disk exhaustion (v1.14.30b),
but push-by-digest streams straight to the registry with fundamentally
different disk profile. Reclaim step gives enough headroom for the
combined amd64+arm64 push case.

Workflow: 7 jobs → 5. docker-publish.yml: 263 → ~110 lines of YAML.

Also:
- timeout-minutes: 90 on build jobs so hung builds fail explicitly
- BUILDKIT_PROGRESS=plain at workflow level for line-by-line arm64 logs
- AGENTS.md §CI quirks documents the Gitea-specific traps
  (upload-artifact@v3-only, dash-not-bash, build-push-action@v7
  multi-arch convention, reclaim requirement)
2026-05-01 12:28:34 +00:00

338 lines
12 KiB
YAML

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