Skip to content

ci(docker): support arm64 builds and multi-arch manifest assembly - #2132

Merged
nullPointerEnjoyer merged 4 commits into
masterfrom
ci-docker-multiarch
Oct 11, 2026
Merged

nullPointerEnjoyer merged 4 commits into
masterfrom
ci-docker-multiarch

Conversation

@erubboli

@erubboli erubboli commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Register the arm64 docker build support on the default branch.

  • build-tools/docker/build.py: --platform (amd64|arm64) and --tag_suffix — tags are suffixed inside build/push, a local unsuffixed alias keeps app-image FROM references working, and the version stays unsuffixed (no double-suffixing).
  • .github/workflows/release_docker.yml: workflow_dispatch inputs (version, platform, tag_suffix, runner) for native arm64 builds (ubuntu-24.04-arm) pushed as <tag>-arm64 suffixed tags; plus an assemble_manifests dispatch mode whose job stitches per-arch tags into multi-arch manifest lists registry-side (docker buildx imagetools create, nothing rebuilt, idempotent amd64 snapshot). Kept inside the same workflow because GitHub validates workflow registrations against the default branch.
  • single-arch release builds (workflow_call) behave exactly as before.

Proven on the release branch: amd64 path untouched (regression dispatch), arm64 build+push dispatch green for v1.4.1 (all 8 images arm64 in registry). Rebased onto the current tip after the upstream history rewrite; the release-only commits (checkpoints, changelogs, arch smoke fixes) are intentionally NOT included here.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

🔍 OpenCodeReview found 8 issue(s) in this PR.

  • ✅ Successfully posted inline: 2 comment(s)
  • 📋 Routed to summary by policy: 4 comment(s)
  • ⏭️ Skipped (overlap with history): 2 comment(s)

other · low

📄 .github/workflows/release_docker.yml (L163-L164)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category other)

A workflow_dispatch on a non-protected ref (e.g. a feature branch) matches neither job condition, so the workflow silently succeeds with zero jobs having run. An operator dispatching from a feature branch gets no signal that nothing was built or assembled. Consider adding an always-running guard job (if: always()) that fails with a clear message when neither the build nor assemble condition matched.

💡 Suggested Change

Before:

  assemble-manifests:
    if: github.event_name == 'workflow_dispatch' && inputs.assemble_manifests == 'true' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))

After:

  dispatch-guard:
    if: always() && github.event_name == 'workflow_dispatch' && needs.result.result == 'skipped'
    needs: [build-docker-images, assemble-manifests]
    runs-on: ubuntu-latest
    steps:
    - name: Fail on fully-skipped dispatch
      run: |
        echo "Dispatch matched no job: use a master/release-/v-tag ref" >&2
        exit 1

other · low

📄 .github/workflows/release_docker.yml (L136-L139)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category other)

The 'platform' input is not validated up front, unlike 'runner' and 'tag_suffix'. build.py's argparse choices do reject unknown values, so this is only a latency/clarity concern (the job checks out, logs in to Docker Hub, and sets up buildx before failing with an argparse error). A quick validation next to the tag_suffix check would fail fast before any registry login.

💡 Suggested Change

Before:

        if [[ -n "$TAG_SUFFIX" ]] && ! [[ "$TAG_SUFFIX" =~ ^[a-zA-Z0-9]([a-zA-Z0-9_.-]{0,127})?$ ]]; then
          echo "invalid tag_suffix '$TAG_SUFFIX'; expected [a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}" >&2
          exit 1
        fi

After:

        if [[ "$PLATFORM" != 'amd64' && "$PLATFORM" != 'arm64' ]]; then
          echo "invalid platform '$PLATFORM'; expected amd64 or arm64" >&2
          exit 1
        fi

other · low

📄 .github/workflows/release_docker.yml (L122-L124)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category other)

The run: scripts rely on the default shell while using bash-only syntax ([[ ]], regex =~, <<< here-strings). Explicitly declaring 'shell: bash' on these steps would keep them working if the default ever changes or the job moves to a runner with a different default shell.

💡 Suggested Change

Before:

    - name: Build and Push Docker images
      env:
        VERSION: ${{ steps.version.outputs.VERSION }}

After:

    - name: Build and Push Docker images
      shell: bash
      env:
        VERSION: ${{ steps.version.outputs.VERSION }}

other · low

📄 .github/workflows/release_docker.yml (L253-L254)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category other)

The build pushes '', 'v' and (for release versions) 'latest', but this verification only inspects 'v'. If the operator supplied the version without the 'v' prefix (the input's own description example is '1.4.1'), the plain '' and shared 'latest' tags are repointed by the assembly yet never checked — a stale or single-arch 'latest' ships silently despite the job's stated intent. Add 'latest' (and optionally "$VERSION") to the verified tag loop.

💡 Suggested Change

Before:

        for repo in node-daemon api-blockchain-scanner-daemon api-web-server wallet-cli wallet-rpc-daemon dns-server; do
          echo "=== $DOCKERHUB_USERNAME/$repo:v$VERSION ==="

After:

        for tag in "v$VERSION" latest; do
          echo "=== $DOCKERHUB_USERNAME/$repo:$tag ==="

Comment thread .github/workflows/release_docker_manifest.yml Outdated
Comment thread .github/workflows/release_docker_manifest.yml Outdated
Comment thread .github/workflows/release_docker_manifest.yml Outdated
Comment thread .github/workflows/release_docker_manifest.yml Outdated
Comment thread .github/workflows/release_docker.yml
Comment thread .github/workflows/release_docker.yml Outdated
Comment thread .github/workflows/release_docker.yml
Comment thread build-tools/docker/build.py
@nullPointerEnjoyer
nullPointerEnjoyer force-pushed the master branch 2 times, most recently from 70bd008 to 3b3867e Compare October 6, 2026 19:00
@erubboli
erubboli force-pushed the ci-docker-multiarch branch from 693ebf8 to ca5dff9 Compare October 6, 2026 19:05
build.py gains --platform (amd64|arm64) and --tag_suffix: tags are
suffixed inside build/push (with a local alias kept unsuffixed so app
image builds can still reference the base images), and the version input
stays unsuffixed to avoid double-suffixing.

release_docker.yml becomes platform-aware via workflow_dispatch inputs
(version, platform, tag_suffix, runner) so arm64 images can be built
natively (ubuntu-24.04-arm) and pushed as '<tag>-arm64' suffixed tags.

The new assemble_manifests input runs a registry-side job that turns
per-arch suffixed tags into multi-arch manifest lists under the
unsuffixed tags (docker buildx imagetools create; nothing rebuilt,
idempotent snapshot of the historical amd64 layout under explicit
'-amd64' tags first). Kept in the same workflow because GitHub validates
workflow registrations against the default branch, so a separate
dispatch-only workflow could not be used from a release branch.

Note: single-arch release builds (workflow_call) behave exactly as before.
@erubboli
erubboli force-pushed the ci-docker-multiarch branch from ca5dff9 to 10473d0 Compare October 6, 2026 19:11
Comment thread .github/workflows/release_docker.yml
Comment thread .github/workflows/release_docker.yml Outdated
Comment thread .github/workflows/release_docker.yml Outdated
Comment thread build-tools/docker/build.py Outdated
Comment thread .github/workflows/release_docker.yml Outdated
Comment thread .github/workflows/release_docker.yml
Comment thread .github/workflows/release_docker.yml
Comment thread build-tools/docker/build.py

@nullPointerEnjoyer nullPointerEnjoyer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved: OCR review converged to 0 findings over 5 cycles, full check suite green, all 12 review threads resolved (permissions least-privilege, runner allowlist, tag_suffix validation, non-amd64 push guard, atomic assembly pre-flight, multi-arch verification, action SHA pins, timeouts, concurrency serialization).

Comment thread .github/workflows/release_docker.yml Outdated
Comment thread .github/workflows/release_docker.yml
Comment thread .github/workflows/release_docker.yml
- workflow_dispatch builds and assembly now require a protected ref
  (master or release-*), so ad-hoc runs from feature branches cannot
  push versioned or latest tags with the repo Docker Hub credentials
- build and assembly jobs serialize behind fixed concurrency groups
- latest is stamped only for semver versions; SHA builds (the dispatch
  default) keep their own tags only, and anything else fails early
- version validation matches the whole value (same rule as the tag
  suffix), so a multi-line input cannot expand into extra tag arguments
- assembly logs both source digests before creating a manifest list
- build.py skips the local base-image alias when no tags were passed
Comment on lines +106 to +109
elif [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
validate "$VERSION"
STAMP_LATEST=true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
For a workflow_dispatch on a protected ref, the 'version' input is accepted as any semver string without being tied to the checked-out ref (unlike the tag path, which derives VERSION from GITHUB_REF). Since builds push '', 'v' and, with --latest, the shared 'latest' tag, an operator (or a compromised token with dispatch rights) can repoint already-released version tags with different content. Consider requiring, for dispatches, that the version matches the ref (tag dispatches) or at least warning/gating when the target tags already exist in the registry.

Comment on lines +225 to +229
echo "amd64 digest of $repo:$tag-amd64:"
docker buildx imagetools inspect "$repo:$tag-amd64" | grep '^Digest:'
echo "arm64 digest of $repo:$tag-arm64:"
docker buildx imagetools inspect "$repo:$tag-arm64" | grep '^Digest:'
docker buildx imagetools create -t "$repo:$tag" "$repo:$tag-amd64" "$repo:$tag-arm64"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
The assemble() function combines whatever 'latest-amd64'/'latest-arm64' manifests currently exist in the registry into the shared production 'latest' and 'v' tags, without verifying they were built from the same commit/version. Two independently dispatched per-arch builds from different commits (as the inline provenance comment acknowledges) can be published as one multi-arch release. The digest logging is post-hoc audit only, not a gate. Consider recording the source digest (e.g. as a workflow output or build-time attestation/label) during each per-arch build and comparing them here before running the final 'imagetools create', or pinning 'create' to the exact digests captured during pre-flight.

Suggestion:

Suggested change
echo "amd64 digest of $repo:$tag-amd64:"
docker buildx imagetools inspect "$repo:$tag-amd64" | grep '^Digest:'
echo "arm64 digest of $repo:$tag-arm64:"
docker buildx imagetools inspect "$repo:$tag-arm64" | grep '^Digest:'
docker buildx imagetools create -t "$repo:$tag" "$repo:$tag-amd64" "$repo:$tag-arm64"
amd64_digest="$(docker buildx imagetools inspect "$repo:$tag-amd64" | grep '^Digest:' | awk '{print $2}')"
arm64_digest="$(docker buildx imagetools inspect "$repo:$tag-arm64" | grep '^Digest:' | awk '{print $2}')
# Compare against the digests recorded by the per-arch builds before repointing the shared tag.
check_digests_match "$repo" "$tag" "$amd64_digest" "$arm64_digest"
docker buildx imagetools create -t "$repo:$tag" "$repo:$tag-amd64@$amd64_digest" "$repo:$tag-arm64@$arm64_digest"

@nullPointerEnjoyer
nullPointerEnjoyer merged commit 5aa5a8e into master Oct 11, 2026
21 checks passed
@nullPointerEnjoyer
nullPointerEnjoyer deleted the ci-docker-multiarch branch October 11, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants