ci: support multi-platform Docker image build (amd64 + arm64)#298
ci: support multi-platform Docker image build (amd64 + arm64)#298FletcherMan merged 3 commits intomainfrom
Conversation
Use docker/build-push-action with QEMU and buildx to build multi-arch images. Mac arm64 users can now pull and run the image natively. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughReplaces manual Docker commands in the GitHub Actions release workflow with official actions, adds multi-arch build/push using buildx and QEMU, extracts VERSION from the tag/ref, switches IMAGE_NAME to GHCR, and uses cache and build args (COMMIT, VERSION). Changes
Sequence Diagram(s)sequenceDiagram
participant User as "User / GitHub\n(workflow_dispatch)"
participant Runner as "GitHub Actions\nrunner"
participant Checkout as "actions/checkout"
participant QEMU as "docker/setup-qemu-action"
participant Buildx as "docker/setup-buildx-action"
participant Login as "docker/login-action"
participant BuildPush as "docker/build-push-action"
participant GHCR as "GHCR (ghcr.io)"
User->>Runner: trigger workflow (tag/ref)
Runner->>Checkout: checkout specified ref
Runner->>QEMU: setup QEMU
Runner->>Buildx: setup Buildx
Runner->>Login: authenticate to GHCR
Runner->>BuildPush: build multi-arch images (COMMIT, VERSION), use cache
BuildPush->>GHCR: push images (version tag, latest)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Allow manually triggering the Docker build from GitHub Actions UI with a tag name input, useful for re-building existing tags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
.github/workflows/docker_release.yml (1)
44-47: Consider guardinglatestso pre-release tags don’t overwrite it.With trigger
morph-v*, tags likemorph-v1.2.0-rc1would also publish:latest(Line 46). This can accidentally promote release candidates.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/docker_release.yml around lines 44 - 47, The workflow currently always publishes the :latest tag; change it to only publish :latest when the computed version has no prerelease suffix by adding a conditional that checks steps.version.outputs.version for a '-' character and only pushes/appends the latest tag when it does not exist; specifically, keep the primary tag ${env.IMAGE_NAME}:${{ steps.version.outputs.version }} but move the publication of ${env.IMAGE_NAME}:latest into a separate step or conditionalized tags block that runs with if: "!contains(steps.version.outputs.version, '-')" (or equivalent expression), referencing env.IMAGE_NAME and steps.version.outputs.version so release candidates like morph-v1.2.0-rc1 will not overwrite :latest.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/docker_release.yml:
- Around line 47-49: Replace the raw github.ref_name in the build-args with the
workflow step output that contains the normalized version so the VERSION
build-arg matches the image tag; i.e. change VERSION=${{ github.ref_name }} to
VERSION=${{
steps.<version_normalize_step_id>.outputs.<normalized_version_output> }} (keep
COMMIT as-is) — confirm the actual step id and output name used earlier in the
workflow (e.g. steps.normalize_version.outputs.normalized or
steps.get_version.outputs.version) and use that exact identifier.
- Around line 24-30: The GHCR login step using docker/login-action@v3 currently
sets username to ${{ github.actor }} which will fail when the PAT in
secrets.PACKAGE_TOKEN belongs to a bot; update the "Log into registry" step to
either (a) switch to using ${{ secrets.GITHUB_TOKEN }} as the password and
ensure workflow permissions include packages: write (recommended), or (b) keep
PACKAGE_TOKEN but replace username with a new secret holding the PAT owner's
username (e.g., secrets.PACKAGE_USERNAME) so the docker/login-action fields
registry, username and password match the token owner; adjust the workflow step
that references registry/username/password accordingly.
---
Nitpick comments:
In @.github/workflows/docker_release.yml:
- Around line 44-47: The workflow currently always publishes the :latest tag;
change it to only publish :latest when the computed version has no prerelease
suffix by adding a conditional that checks steps.version.outputs.version for a
'-' character and only pushes/appends the latest tag when it does not exist;
specifically, keep the primary tag ${env.IMAGE_NAME}:${{
steps.version.outputs.version }} but move the publication of
${env.IMAGE_NAME}:latest into a separate step or conditionalized tags block that
runs with if: "!contains(steps.version.outputs.version, '-')" (or equivalent
expression), referencing env.IMAGE_NAME and steps.version.outputs.version so
release candidates like morph-v1.2.0-rc1 will not overwrite :latest.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 88dd8ec7-d958-45e0-b724-46e14c0d38a4
📒 Files selected for processing (1)
.github/workflows/docker_release.yml
| - name: Log into registry | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.PACKAGE_TOKEN }} | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For docker/login-action@v3 with ghcr.io, does username need to match the owner of a PAT used as password, and is github.actor safe when the PAT belongs to a bot/service account?
💡 Result:
Yes—when authenticating to GHCR (ghcr.io) with a PAT, the username is expected to be the GitHub username of the account that owns the token (i.e., the user/bot that created the PAT). GitHub’s docker/OCI registry login examples explicitly treat USERNAME as “your GitHub username.” [1][2]
github.actor is only correct when the credential is also for the actor (most commonly when using secrets.GITHUB_TOKEN, which is tied to the workflow run’s context and the action examples use github.actor). [3] If your password is a PAT that belongs to a bot/service account, then using username: ${{ github.actor }} is not safe/reliable (it may be a human user, dependabot, etc.)—set username to the bot/service account’s login instead. [1][2][3]
Sources
[1] GitHub Docs – Working with the Container registry (GHCR) (docker login example) (docs.github.com)
[2] GitHub Docs – Working with the Docker registry (Enterprise Server) (explicitly “replace USERNAME with your GitHub username”) (docs.github.com)
[3] docker/login-action README (GHCR example uses username: ${{ github.actor }} with GITHUB_TOKEN) (github.com)
Citations:
- 1: https://docs.github.com/packages/getting-started-with-github-container-registry/about-github-container-registry
- 2: https://docs.github.com/en/enterprise-server%403.14/packages/working-with-a-github-packages-registry/working-with-the-docker-registry?utm_source=openai
- 3: https://github.com/docker/login-action
GHCR authentication will fail if PACKAGE_TOKEN is a bot/service account PAT and tag is pushed by a different user.
When authenticating to GHCR with a PAT, the username must match the account that owns the token. Using ${{ github.actor }} (line 28) works only if the PAT owner and tag pusher are the same. If secrets.PACKAGE_TOKEN is a bot account's PAT, any tag pushed by a human user will fail login.
Recommended fix: Either switch to secrets.GITHUB_TOKEN with explicit package permissions, or store the bot account username in a separate secret.
Option 1: Use GITHUB_TOKEN (recommended)
jobs:
push:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
steps:
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
- password: ${{ secrets.PACKAGE_TOKEN }}
+ password: ${{ secrets.GITHUB_TOKEN }}Option 2: Keep PAT but use correct username
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ghcr.io
- username: ${{ github.actor }}
+ username: ${{ secrets.PACKAGE_USERNAME }}
password: ${{ secrets.PACKAGE_TOKEN }}Add PACKAGE_USERNAME secret with the bot account's username.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Log into registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.PACKAGE_TOKEN }} | |
| - name: Log into registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ secrets.PACKAGE_USERNAME }} | |
| password: ${{ secrets.PACKAGE_TOKEN }} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/docker_release.yml around lines 24 - 30, The GHCR login
step using docker/login-action@v3 currently sets username to ${{ github.actor }}
which will fail when the PAT in secrets.PACKAGE_TOKEN belongs to a bot; update
the "Log into registry" step to either (a) switch to using ${{
secrets.GITHUB_TOKEN }} as the password and ensure workflow permissions include
packages: write (recommended), or (b) keep PACKAGE_TOKEN but replace username
with a new secret holding the PAT owner's username (e.g.,
secrets.PACKAGE_USERNAME) so the docker/login-action fields registry, username
and password match the token owner; adjust the workflow step that references
registry/username/password accordingly.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
.github/workflows/docker_release.yml (2)
55-57:⚠️ Potential issue | 🟡 MinorUse the normalized version output for the
VERSIONbuild arg.Line 57 passes the raw tag/ref, but image tags use
steps.version.outputs.version, so metadata becomes inconsistent.Suggested fix
build-args: | COMMIT=${{ github.sha }} - VERSION=${{ inputs.tag || github.ref_name }} + VERSION=${{ steps.version.outputs.version }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/docker_release.yml around lines 55 - 57, Replace the raw ref/tag used for the VERSION build-arg with the normalized output from the version step: change the VERSION build-arg so it uses steps.version.outputs.version instead of inputs.tag or github.ref_name (keep COMMIT as-is); update the build-args block that defines VERSION to reference steps.version.outputs.version so image tags and metadata remain consistent with the versioning step.
31-36:⚠️ Potential issue | 🟠 MajorAlign GHCR username with the token owner.
Line 35 uses
${{ github.actor }}while Line 36 uses a PAT (secrets.PACKAGE_TOKEN). This is unreliable when the PAT belongs to a bot/service account instead of the triggering actor.Suggested fix
- name: Log into registry uses: docker/login-action@v3 with: registry: ghcr.io - username: ${{ github.actor }} + username: ${{ secrets.PACKAGE_USERNAME }} password: ${{ secrets.PACKAGE_TOKEN }}For docker/login-action@v3 to ghcr.io using a PAT, must `username` match the PAT owner account, and is `${{ github.actor }}` only safe when using `${{ secrets.GITHUB_TOKEN }}`?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/docker_release.yml around lines 31 - 36, The "Log into registry" step uses docker/login-action@v3 with username: ${{ github.actor }} while the password is a PAT (secrets.PACKAGE_TOKEN); change the username to the actual PAT owner instead of github.actor so the PAT and username match. Update the workflow step that uses docker/login-action@v3 (step name "Log into registry") to read the username from a secret or known owner (e.g., a new secrets.PACKAGE_TOKEN_USERNAME or github.repository_owner if the PAT belongs to the repo owner) so the PAT owner and username align when authenticating to ghcr.io.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/docker_release.yml:
- Around line 38-44: The manual-dispatch flow can output an incorrect COMMIT
(github.sha) when inputs.tag is checked out; update the workflow to capture the
actual checked-out commit by running git rev-parse HEAD after the checkout and
write it to the outputs (similar to how TAG/VERSION are handled). Specifically,
in the step that extracts version (where TAG and VERSION are set) add a command
to compute COMMIT="$(git rev-parse HEAD)" and echo "commit=${COMMIT}" >>
$GITHUB_OUTPUT so downstream uses the actual checked-out commit SHA instead of
github.sha.
---
Duplicate comments:
In @.github/workflows/docker_release.yml:
- Around line 55-57: Replace the raw ref/tag used for the VERSION build-arg with
the normalized output from the version step: change the VERSION build-arg so it
uses steps.version.outputs.version instead of inputs.tag or github.ref_name
(keep COMMIT as-is); update the build-args block that defines VERSION to
reference steps.version.outputs.version so image tags and metadata remain
consistent with the versioning step.
- Around line 31-36: The "Log into registry" step uses docker/login-action@v3
with username: ${{ github.actor }} while the password is a PAT
(secrets.PACKAGE_TOKEN); change the username to the actual PAT owner instead of
github.actor so the PAT and username match. Update the workflow step that uses
docker/login-action@v3 (step name "Log into registry") to read the username from
a secret or known owner (e.g., a new secrets.PACKAGE_TOKEN_USERNAME or
github.repository_owner if the PAT belongs to the repo owner) so the PAT owner
and username align when authenticating to ghcr.io.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 70307d3c-28aa-4c0a-ac32-d43928bf7e5b
📒 Files selected for processing (1)
.github/workflows/docker_release.yml
Use git rev-parse HEAD for COMMIT and stripped version for VERSION build-arg, so they are correct in both tag-push and workflow_dispatch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
.github/workflows/docker_release.yml (2)
7-11: Consider adding input validation for tag format.If a user enters a tag without the
morph-vprefix (e.g.,v2.2.0), thesedcommand on line 42 won't strip anything, resulting in the raw input being used as VERSION. This could publish images with unexpected tag names.💡 Add pattern validation
workflow_dispatch: inputs: tag: description: 'Tag name to build (e.g. morph-v2.2.0)' required: true + type: stringThen add validation in the extract step:
- name: Extract version and commit id: meta run: | TAG="${{ inputs.tag || github.ref_name }}" if [[ ! "$TAG" =~ ^morph-v ]]; then echo "::error::Tag must start with 'morph-v' prefix" exit 1 fi VERSION=$(echo "$TAG" | sed -e 's/^morph-v//') # ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/docker_release.yml around lines 7 - 11, Validate the incoming workflow_dispatch input "tag" (used in the "Extract version and commit" step with id "meta") before stripping the prefix: check the TAG variable (computed from inputs.tag or github.ref_name) matches the required ^morph-v prefix, emit an error and exit non‑zero if it does not, and only then run the sed that sets VERSION by removing the morph-v prefix; reference the TAG and VERSION variables and the meta step to locate where to add the validation.
54-56: Consider conditional:latesttag for manual dispatch builds.When manually building an older tag (e.g.,
morph-v2.1.0whilemorph-v2.2.0is current), this will overwrite:latestwith the older version. This may be intentional for hotfixes, but could cause confusion.💡 Conditional latest tag
If
:latestshould only be pushed for tag-push events (not manual rebuilds of old versions):tags: | ${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} - ${{ env.IMAGE_NAME }}:latest + ${{ github.event_name == 'push' && format('{0}:latest', env.IMAGE_NAME) || '' }}Or add an input to control this behavior explicitly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/docker_release.yml around lines 54 - 56, The current tags block always pushes :latest alongside ${ env.IMAGE_NAME }:${{ steps.meta.outputs.version }}, which can overwrite latest on manual_dispatch builds; update the tags list to only include the :latest tag conditionally (e.g., wrap ${{ env.IMAGE_NAME }}:latest in an if that checks the event or ref) such as using github.event_name == 'push' or startsWith(github.ref, 'refs/tags/') so that :latest is only pushed for real tag pushes (alternatively add a workflow input like push_latest and gate the :latest tag on that input); target the tags values for env.IMAGE_NAME and steps.meta.outputs.version when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In @.github/workflows/docker_release.yml:
- Around line 7-11: Validate the incoming workflow_dispatch input "tag" (used in
the "Extract version and commit" step with id "meta") before stripping the
prefix: check the TAG variable (computed from inputs.tag or github.ref_name)
matches the required ^morph-v prefix, emit an error and exit non‑zero if it does
not, and only then run the sed that sets VERSION by removing the morph-v prefix;
reference the TAG and VERSION variables and the meta step to locate where to add
the validation.
- Around line 54-56: The current tags block always pushes :latest alongside ${
env.IMAGE_NAME }:${{ steps.meta.outputs.version }}, which can overwrite latest
on manual_dispatch builds; update the tags list to only include the :latest tag
conditionally (e.g., wrap ${{ env.IMAGE_NAME }}:latest in an if that checks the
event or ref) such as using github.event_name == 'push' or
startsWith(github.ref, 'refs/tags/') so that :latest is only pushed for real tag
pushes (alternatively add a workflow input like push_latest and gate the :latest
tag on that input); target the tags values for env.IMAGE_NAME and
steps.meta.outputs.version when making this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4db2a8ae-67f8-4260-9dec-91a4df172c46
📒 Files selected for processing (1)
.github/workflows/docker_release.yml
* ci: support multi-platform Docker image build (amd64 + arm64) (#298) * ci: support multi-platform Docker image build (amd64 + arm64) Use docker/build-push-action with QEMU and buildx to build multi-arch images. Mac arm64 users can now pull and run the image natively. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: add workflow_dispatch for manual Docker image build Allow manually triggering the Docker build from GitHub Actions UI with a tag name input, useful for re-building existing tags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: fix incorrect COMMIT and VERSION on manual dispatch Use git rev-parse HEAD for COMMIT and stripped version for VERSION build-arg, so they are correct in both tag-push and workflow_dispatch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: fletcher.fan <fletcher.fan@bitget.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Fix RLP decoding for MorphTx (#299) * implement version-aware RLP decoding for MorphTx * fix morph tx * pruner: fall back to disk snapshot root when journal is missing (#300) * pruner: fall back to disk snapshot root when journal is missing When geth is killed uncleanly (SIGKILL before BlockChain.Stop writes the snapshot journal), prune-state fails with: WARN Loaded snapshot journal diskroot=XXX diffs=missing ERROR head doesn't match snapshot: have XXX, want YYY NewPruner now reads the persisted disk snapshot root via rawdb.ReadSnapshotRoot and retries snapshot initialisation with that root when the normal head-based init fails. Prune() then uses the disk root as the pruning target directly, bypassing the requirement for 128 in-memory diff layers that cannot exist when the journal was not written. Normal flow (clean shutdown, journal present) is unchanged. Made-with: Cursor * pruner: fix Cap panic on disk-layer-only tree and add generation wait log Two follow-up fixes to the journal-missing fallback (56ae344): 1. Skip snaptree.Cap(root, 0) when root is already the disk layer. Cap requires a diffLayer as its target; calling it on a disk-layer-only tree (which is exactly what the fallback produces) returns "snapshot is disk layer" and aborts after all the heavy bloom-filter and DB-sweep work is done. Guard with DiskRoot() != root. 2. Add log lines around the fallback snapshot.New() call to make it visible when snapshot generation must be resumed (async=false blocks until generation finishes, which can take hours for large state). * pruner: rename diskRoot to snapDiskRoot to avoid confusion with diskStateRoot --------- Co-authored-by: corey <corey.zhang@bitget.com> * Revert "pruner: fall back to disk snapshot root when journal is missing (#300)" (#309) This reverts commit b3c5552. Co-authored-by: corey <corey.zhang@bitget.com> * tracers: fix Morph fee-token tracing paths (#308) * tracers: fix Morph fee-token tracing paths Keep Morph fee-token system calls bracketed consistently, forward system-call hooks through mux tracers, and make traceCall precredit alt-fee balances so tracing matches execution more closely. Constraint: Preserve user-visible tracer output while keeping prestate and traceCall behavior correct for Morph fee-token transactions Confidence: medium Scope-risk: moderate Not-tested: Full eth/tracers/internal/tracetest suite still has pre-existing fixture and VM failures on this branch * tracers: fix prestateTracer account discovery when DisableStorage is set * tracers: harden Morph fee-token trace edge cases Prevent flatCallTracer from touching hidden system-call frames and keep traceCall's synthetic fee-token precredits out of prestate views so debug RPCs stay stable and prestate output matches chain state. Constraint: Preserve Morph alt-fee trace execution without leaking synthetic prestate or hidden system-call frames Confidence: high Scope-risk: moderate * core: require balanced system-call trace hooks Only bracket fee-token helper calls when both start and end hooks are present so partial tracer wiring cannot leak system-call depth across a trace. Constraint: Preserve existing V2-over-legacy hook selection while restoring balanced start/end semantics Confidence: high Scope-risk: narrow Not-tested: Full core package outside TestStartSystemCallTrace * fix: handle nil parameter in morph_diskRoot RPC to prevent panic (#311) When morph_diskRoot is called without parameters, blockNrOrHash is nil, causing a nil pointer dereference crash. Default to latest block when no parameter is provided, consistent with other eth RPC methods. * pruner: use teeWriter and HEAD as prune target, fix genesis root validation (#310) * pruner: use teeWriter and HEAD as prune target, fix genesis root validation - Add teeWriter to persist trie nodes to disk during GenerateTrie, ensuring pruning works correctly even after unclean shutdowns. - Use HEAD directly as the pruning target instead of HEAD-127, eliminating unnecessary height rollback on L2 chains where reorgs don't occur. - Resolve genesis root via ReadDiskStateRoot in extractGenesis so that zkTrie roots (overridden via GenesisStateRoot) are correctly mapped to the actual MPT disk root. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * handle error * remove accidentally committed local test zip Keep local-test.zip untracked and out of repository history from this point. Made-with: Cursor * pruner: enforce genesis disk-root mapping Treat missing or invalid disk-state-root mapping for genesis as an explicit error during pruning instead of silently falling back. Made-with: Cursor --------- Co-authored-by: corey <corey.zhang@bitget.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: fletcher.fan <fletcher.fan@bitget.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Segue <huoda.china@163.com> Co-authored-by: corey <coreyx1992@gmail.com> Co-authored-by: corey <corey.zhang@bitget.com> Co-authored-by: panos <pan107104@outlook.com>
Summary
ubuntu-latestrunnerlinux/amd64andlinux/arm64as a single multi-arch manifestdocker build/tag/pushwithdocker/build-push-action@v6type=gha) to speed up subsequent buildsTest plan
morph-v*tag and verify both amd64 and arm64 images are published to ghcr.io🤖 Generated with Claude Code
Summary by CodeRabbit