name: Publish Docker Image on: push: tags: - 'v*' # 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). Building both # architectures of both variants on a single runner exhausted disk around the # nodejs dpkg unpack / git-lfs layer export. To fix this: # * smoke test (amd64 only, load into daemon) runs on its own runner # * each push target (variant × arch) runs on its own runner, pushes by # digest (no local image store), uploads digest as an artifact # * a merge job composes the multi-arch manifest with `imagetools create` # Per-runner disk pressure is now one-quarter of the old single-job peak. 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 - 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: 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 # ── Per-arch push (by digest, no local image) ─────────────────────── build-base: runs-on: ubuntu-latest needs: smoke-base container: image: catthehacker/ubuntu:act-latest strategy: fail-fast: false matrix: platform: - linux/amd64 - linux/arm64 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: Derive platform slug id: platform run: | PLATFORM_PAIR="${{ matrix.platform }}" echo "pair=${PLATFORM_PAIR//\//-}" >> $GITHUB_OUTPUT - name: Set up QEMU if: matrix.platform != 'linux/amd64' uses: docker/setup-qemu-action@v4 - 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: Build and push by digest id: build uses: docker/build-push-action@v7 with: context: . platforms: ${{ matrix.platform }} outputs: type=image,name=${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox,push-by-digest=true,name-canonical=true,push=true - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v4 with: name: digests-base-${{ steps.platform.outputs.pair }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 build-omos: runs-on: ubuntu-latest needs: smoke-omos container: image: catthehacker/ubuntu:act-latest strategy: fail-fast: false matrix: platform: - linux/amd64 - linux/arm64 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: Derive platform slug id: platform run: | PLATFORM_PAIR="${{ matrix.platform }}" echo "pair=${PLATFORM_PAIR//\//-}" >> $GITHUB_OUTPUT - name: Set up QEMU if: matrix.platform != 'linux/amd64' uses: docker/setup-qemu-action@v4 - 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: Build and push by digest id: build uses: docker/build-push-action@v7 with: context: . platforms: ${{ matrix.platform }} build-args: | INSTALL_OMOS=true outputs: type=image,name=${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox,push-by-digest=true,name-canonical=true,push=true - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v4 with: name: digests-omos-${{ steps.platform.outputs.pair }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 # ── Merge per-arch digests into multi-arch tags ───────────────────── merge-base: runs-on: ubuntu-latest needs: build-base container: image: catthehacker/ubuntu:act-latest steps: - name: Force IPv4 for Docker Hub run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf - name: Download digests uses: actions/download-artifact@v4 with: path: /tmp/digests pattern: digests-base-* merge-multiple: true - 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: Create manifest list and push working-directory: /tmp/digests run: | docker buildx imagetools create \ -t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }} \ -t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest \ $(printf '${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox@sha256:%s ' *) - name: Inspect image run: | docker buildx imagetools inspect \ ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }} merge-omos: runs-on: ubuntu-latest needs: build-omos container: image: catthehacker/ubuntu:act-latest steps: - name: Force IPv4 for Docker Hub run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf - name: Download digests uses: actions/download-artifact@v4 with: path: /tmp/digests pattern: digests-omos-* merge-multiple: true - 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: Create manifest list and push working-directory: /tmp/digests run: | docker buildx imagetools create \ -t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos \ -t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-omos \ $(printf '${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox@sha256:%s ' *) - name: Inspect image run: | docker buildx imagetools inspect \ ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos update-description: runs-on: ubuntu-latest needs: [merge-base, merge-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