From fbc6ddec0174171b754659c734ec1319985ae9f8 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Thu, 21 May 2026 14:37:33 +0100 Subject: [PATCH 1/3] feat(release): publish multi-arch docker images on tagged releases (V2-298) Tagged `vX.Y.Z` pushes now publish `withautonomi/indelible` and `withautonomi/antd` to Docker Hub and GHCR for `linux/amd64` + `linux/arm64`, both as `:vX.Y.Z` and `:latest`. antd images use the resolved ant-sdk release tag as their version. Also extends `workflow_dispatch` with a `version` input so the docker publish path can be exercised from a branch without cutting a real release tag. In dry-run mode the binary `build` and GitHub `release` jobs are skipped, `:latest` is not touched, and the antd image is namespaced under the dry-run version rather than its real antd tag. README's compose Quick Start no longer carries the "image not yet available" caveat. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 82 +++++++++++++++++++---------------- README.md | 2 +- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 396a5fd..e32cc11 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,6 +6,10 @@ on: - "v*" workflow_dispatch: inputs: + version: + description: "Dry-run version for docker image tags (e.g. 0.0.0-dryrun-1). Only : is pushed; :latest is reserved for real tag releases." + required: false + default: "0.0.0-dryrun" antd_version: description: "ant-sdk release tag to bundle (e.g. v0.2.0). Leave empty to read .antd-version file." required: false @@ -21,18 +25,22 @@ jobs: version: ${{ steps.meta.outputs.version }} prerelease: ${{ steps.meta.outputs.prerelease }} steps: - - name: Parse tag + - name: Resolve version id: meta run: | - tag="${GITHUB_REF#refs/tags/}" - version="${tag#v}" + if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then + tag="${GITHUB_REF#refs/tags/}" + version="${tag#v}" + else + version="${{ inputs.version }}" + fi echo "version=${version}" >> $GITHUB_OUTPUT if [[ "$version" == *-* ]]; then echo "prerelease=true" >> $GITHUB_OUTPUT else echo "prerelease=false" >> $GITHUB_OUTPUT fi - echo "Tag: $tag, Version: $version, Prerelease: $(grep prerelease $GITHUB_OUTPUT | tail -1 | cut -d= -f2)" + echo "Event: ${GITHUB_EVENT_NAME}, Version: $version" resolve-antd-version: name: Resolve antd version @@ -71,6 +79,7 @@ jobs: build: name: Build (${{ matrix.goos }}/${{ matrix.goarch }}) + if: github.event_name == 'push' runs-on: ${{ matrix.os }} needs: [release-meta, resolve-antd-version] strategy: @@ -147,6 +156,7 @@ jobs: release: name: Create Release + if: github.event_name == 'push' needs: [build, release-meta, resolve-antd-version] runs-on: ubuntu-latest steps: @@ -170,12 +180,8 @@ jobs: body: | Bundled antd daemon: `${{ needs.resolve-antd-version.outputs.antd_tag }}` - # Multi-arch Docker images. The build runs on every tag and validates the - # cross-compile; the push step is gated off until V2-298 lands the Docker Hub - # org + GHCR setup + credentials. Once V2-298 flips `push: true` and adds the - # login steps, this job becomes the actual release-publish path. docker: - name: Docker (multi-arch build, push gated by V2-298) + name: Docker (multi-arch build + push) needs: [release-meta, resolve-antd-version] runs-on: ubuntu-latest permissions: @@ -188,51 +194,53 @@ jobs: - uses: docker/setup-buildx-action@v3 - # V2-298: uncomment the two login steps below and flip both build-push - # actions' `push:` to `true` once DOCKERHUB_USERNAME + DOCKERHUB_TOKEN - # secrets are configured. - # - # - name: Log in to GHCR - # uses: docker/login-action@v3 - # with: - # registry: ghcr.io - # username: ${{ github.actor }} - # password: ${{ secrets.GITHUB_TOKEN }} - # - # - name: Log in to Docker Hub - # uses: docker/login-action@v3 - # with: - # username: ${{ secrets.DOCKERHUB_USERNAME }} - # password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build indelible (linux/amd64,linux/arm64) + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push indelible (linux/amd64,linux/arm64) uses: docker/build-push-action@v6 with: context: . - push: false # V2-298 flips to true + push: true platforms: linux/amd64,linux/arm64 build-args: | VERSION=${{ needs.release-meta.outputs.version }} tags: | ghcr.io/withautonomi/indelible:${{ needs.release-meta.outputs.version }} - ghcr.io/withautonomi/indelible:latest withautonomi/indelible:${{ needs.release-meta.outputs.version }} - withautonomi/indelible:latest + ${{ github.event_name == 'push' && 'ghcr.io/withautonomi/indelible:latest' || '' }} + ${{ github.event_name == 'push' && 'withautonomi/indelible:latest' || '' }} cache-from: type=gha,scope=docker-release cache-to: type=gha,scope=docker-release,mode=max - - name: Build antd companion (linux/amd64 — arm64 blocked on V2-275) + - name: Build and push antd companion (linux/amd64,linux/arm64) uses: docker/build-push-action@v6 with: context: ./deploy/antd - push: false # V2-298 flips to true - platforms: linux/amd64 + push: true + platforms: linux/amd64,linux/arm64 build-args: | ANTD_VERSION=${{ needs.resolve-antd-version.outputs.antd_tag }} + # On dispatch (dry-run) the antd image is tagged with the indelible + # dry-run version, not the antd version, so it doesn't pollute the + # antd release tag namespace. On a real tag push it's tagged with + # the resolved antd version + `latest`. tags: | - ghcr.io/withautonomi/antd:${{ needs.resolve-antd-version.outputs.antd_tag }} - ghcr.io/withautonomi/antd:latest - withautonomi/antd:${{ needs.resolve-antd-version.outputs.antd_tag }} - withautonomi/antd:latest + ${{ github.event_name == 'push' && format('ghcr.io/withautonomi/antd:{0}', needs.resolve-antd-version.outputs.antd_tag) || '' }} + ${{ github.event_name == 'push' && format('withautonomi/antd:{0}', needs.resolve-antd-version.outputs.antd_tag) || '' }} + ${{ github.event_name == 'workflow_dispatch' && format('ghcr.io/withautonomi/antd:{0}', needs.release-meta.outputs.version) || '' }} + ${{ github.event_name == 'workflow_dispatch' && format('withautonomi/antd:{0}', needs.release-meta.outputs.version) || '' }} + ${{ github.event_name == 'push' && 'ghcr.io/withautonomi/antd:latest' || '' }} + ${{ github.event_name == 'push' && 'withautonomi/antd:latest' || '' }} cache-from: type=gha,scope=docker-release-antd cache-to: type=gha,scope=docker-release-antd,mode=max diff --git a/README.md b/README.md index e53da83..0783270 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ cd indelible export INDELIBLE_JWT_SECRET=$(openssl rand -hex 32) export INDELIBLE_WALLET_ENCRYPTION_KEY=$(openssl rand -hex 32) -# Pull and run (when the published image is available; tracked as V2-298). +# Pull and run published images (multi-arch, mirrored on Docker Hub + GHCR): docker compose up -d # Or build locally from source: From 9db423e2edc1fd4bf97647d294aab940038688ad Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Thu, 21 May 2026 14:46:22 +0100 Subject: [PATCH 2/3] ci(docker): run docker build + smoke test on PRs too Previously gated to push events only, with a comment claiming the ~5min cost wasn't worth running per PR. In practice the buildx GHA cache makes warm runs much cheaper, and catching Dockerfile/cross-compile breakage on the PR is more valuable than catching it post-merge on master. Also removes the outdated mention of V2-298 from the comment. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fef8cd..a483a91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -192,18 +192,13 @@ jobs: retention-days: 1 # Validates the Dockerfile + .dockerignore + buildx cross-compile path on - # every PR. Builds amd64 locally for a smoke test (start → /health → uid 65532) - # and also runs the multi-arch (amd64+arm64) build without loading, just to - # confirm the arm64 cross-compile still succeeds. No image is pushed here — - # publishing is handled in release.yml + V2-298. - # Docker build is master-only — multi-arch buildx + amd64 smoke test is - # ~5min and runs against ghcr's cache. We don't need it on every PR; use - # `make docker` locally or run scripts/ci-dev1.sh for full coverage before - # merging anything that touches Dockerfile/deploy/. + # every PR and push. Builds amd64 locally for a smoke test + # (start → /health → uid 65532) and also runs the multi-arch (amd64+arm64) + # build without loading, just to confirm the arm64 cross-compile still + # succeeds. No image is pushed here — publishing is handled in release.yml. docker: name: Docker (build + smoke test) runs-on: ubuntu-latest - if: github.event_name == 'push' steps: - uses: actions/checkout@v4 From 540a17889ccfcf8bb92833c13683b1cb4007848a Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Thu, 21 May 2026 18:21:21 +0100 Subject: [PATCH 3/3] fix(release): gate :latest on stable releases + harden dispatch input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses three review comments on PR #64: * `:latest` (4 image refs) is now gated on `prerelease == 'false'` in addition to event_name being a tag push. Pushing `v1.0.0-rc1` no longer overwrites stable `:latest` for either indelible or antd. * `workflow_dispatch` `version` input is now `required: true` with a sentinel default of `REPLACE-ME`, and `release-meta` fails fast if the sentinel survives to runtime. This prevents accidental dry-runs from silently overwriting a prior `:0.0.0-dryrun` tag. * `inputs.version` is no longer interpolated directly into the shell heredoc — it's bound via `env: INPUT_VERSION:` and dereferenced as a shell variable instead, removing the standard GHA-injection footgun. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e32cc11..80554b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,9 +7,9 @@ on: workflow_dispatch: inputs: version: - description: "Dry-run version for docker image tags (e.g. 0.0.0-dryrun-1). Only : is pushed; :latest is reserved for real tag releases." - required: false - default: "0.0.0-dryrun" + description: "Dry-run version for docker image tags (e.g. 0.0.0-dryrun-1). Only : is pushed; :latest is reserved for real tag releases. Bump the suffix between runs — duplicate values silently overwrite the prior dry-run tag." + required: true + default: "REPLACE-ME" antd_version: description: "ant-sdk release tag to bundle (e.g. v0.2.0). Leave empty to read .antd-version file." required: false @@ -27,12 +27,18 @@ jobs: steps: - name: Resolve version id: meta + env: + INPUT_VERSION: ${{ inputs.version }} run: | if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then tag="${GITHUB_REF#refs/tags/}" version="${tag#v}" else - version="${{ inputs.version }}" + if [[ -z "${INPUT_VERSION}" || "${INPUT_VERSION}" == "REPLACE-ME" ]]; then + echo "::error::workflow_dispatch requires a version input (saw '${INPUT_VERSION}')" + exit 1 + fi + version="${INPUT_VERSION}" fi echo "version=${version}" >> $GITHUB_OUTPUT if [[ "$version" == *-* ]]; then @@ -218,8 +224,8 @@ jobs: tags: | ghcr.io/withautonomi/indelible:${{ needs.release-meta.outputs.version }} withautonomi/indelible:${{ needs.release-meta.outputs.version }} - ${{ github.event_name == 'push' && 'ghcr.io/withautonomi/indelible:latest' || '' }} - ${{ github.event_name == 'push' && 'withautonomi/indelible:latest' || '' }} + ${{ github.event_name == 'push' && needs.release-meta.outputs.prerelease == 'false' && 'ghcr.io/withautonomi/indelible:latest' || '' }} + ${{ github.event_name == 'push' && needs.release-meta.outputs.prerelease == 'false' && 'withautonomi/indelible:latest' || '' }} cache-from: type=gha,scope=docker-release cache-to: type=gha,scope=docker-release,mode=max @@ -240,7 +246,7 @@ jobs: ${{ github.event_name == 'push' && format('withautonomi/antd:{0}', needs.resolve-antd-version.outputs.antd_tag) || '' }} ${{ github.event_name == 'workflow_dispatch' && format('ghcr.io/withautonomi/antd:{0}', needs.release-meta.outputs.version) || '' }} ${{ github.event_name == 'workflow_dispatch' && format('withautonomi/antd:{0}', needs.release-meta.outputs.version) || '' }} - ${{ github.event_name == 'push' && 'ghcr.io/withautonomi/antd:latest' || '' }} - ${{ github.event_name == 'push' && 'withautonomi/antd:latest' || '' }} + ${{ github.event_name == 'push' && needs.release-meta.outputs.prerelease == 'false' && 'ghcr.io/withautonomi/antd:latest' || '' }} + ${{ github.event_name == 'push' && needs.release-meta.outputs.prerelease == 'false' && 'withautonomi/antd:latest' || '' }} cache-from: type=gha,scope=docker-release-antd cache-to: type=gha,scope=docker-release-antd,mode=max