Skip to content

👷 fix: Build the arm64 Image on a Native Runner So Publishing Stops Stalling Under QEMU - #206

Merged
danny-avila merged 1 commit into
mainfrom
pr/native-arm64
Oct 28, 2026
Merged

danny-avila merged 1 commit into
mainfrom
pr/native-arm64

Conversation

@danny-avila

@danny-avila danny-avila commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request Template

⚠️ Before Submitting a PR, Please Review:

  • Please ensure that you have thoroughly read and understood the Contributing Docs before submitting your Pull Request.

⚠️ Documentation Updates Notice:

  • Kindly note that documentation updates are managed in this repository: librechat.ai

Summary

The publish workflow built linux/amd64 and linux/arm64 in one job on an amd64 runner, emulating arm64 with QEMU. bun install stalls under that emulation:

#205's 30-minute timeout stops a stall from holding the publish queue for 6 hours, but a stalled run still publishes nothing.

Native runners. Each architecture now builds on a runner of that architecture: ubuntu-latest for amd64 and ubuntu-24.04-arm for arm64, which is free for public repositories. This is the pattern LibreChat's own docker-publish.yml moved to for the same reason; it measured QEMU 5–6x slower on its images.

  • Prepare tags (one job) computes the tags and labels once with docker/metadata-action, so both architectures carry the same labels.
  • Build (amd64) and Build (arm64) each build their platform and push it untagged, by digest (push-by-digest=true,name-canonical=true), then upload the digest as an artifact.
  • Merge and tag downloads both digests and creates the tagged multi-platform image with docker buildx imagetools create. The image has the same shape as today's latest: an index with the amd64 and arm64 images and their two provenance attestations. One of the reviewers compared it with LibreChat's merged images.

No partial images.

  • The merge runs only when both legs succeeded, and it also checks that it found exactly two digests. If one is missing, it says that the digest artifacts may have expired and to use "Re-run all jobs".
  • fail-fast: false: a failing leg no longer cancels the healthy one, so "Re-run failed jobs" can reuse the healthy leg's digest. The digest artifacts are kept for 7 days (each is one empty file).
  • The merge refuses to push a manifest list with no tags.

latest is decided right before it is pushed. The rule from #205 is unchanged: latest is tagged only when this commit is on main and no later commit on main changed the build context. But the check now runs in the merge job, just before imagetools create, not at the start of the run.

Releases no longer tag latest. This was already happening on main. docker/metadata-action defaults to flavor: latest=auto, which adds latest to every non-prerelease semver tag, skipping the head check. A release cut from an older commit, such as a backport, would move latest back. A release run also isn't queued behind main's runs, since its ref is the tag. With flavor: latest=false, the head check in the merge is the only way latest is tagged, and main's push run for the released commit still tags it.

Runs that can't publish.

  • A publishing run (push or release) that computes no tags now fails with an error. Before, a release tag that isn't semver (v1.1, say) gave no tags. push: true without tags made buildx fail, but only after building both platforms.
  • A manual run on a branch other than main has no tags (the sha tag is for the default branch). It now builds both architectures without logging in or pushing, and skips the merge: a dry run for testing Dockerfile or workflow changes.

One label changes. org.opencontainers.image.version is now the commit sha on main. It was latest, the first tag, which stops describing the image as soon as latest moves on. Releases still get their version.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

  • I ran the merge step's exact script under bash -eo pipefail with a stub docker that prints its arguments:

    Case Result
    Two digests, latest=true imagetools create with the sha tag and :latest
    Two digests, latest=false The sha tag only
    Release (1.2.0, 1.2, 1) The three semver tags, no latest
    No tags Fails: "refusing to push an untagged manifest list"
    One digest Fails: "Expected 2 platform digests … found 1"
    Digest artifacts expired (no directory) Fails: "found 0 … use Re-run all jobs"
  • The head check is ⬆️ chore: Move the Workflows to the Current Major Actions, Publish on Any Build Input and Keep latest on Main's Newest Image #205's script, moved into the merge job unchanged. ⬆️ chore: Move the Workflows to the Current Major Actions, Publish on Any Build Input and Keep latest on Main's Newest Image #205 has its 19-case table.

  • The export step fails when the build reports no digest, and writes the digest file otherwise.

  • The workflow parses as YAML, and the parsed jobs, needs, if conditions, outputs and fail-fast are as intended.

  • Two independent reviews read the workflow against GitHub's and the actions' documentation and source: one for Actions and buildx semantics, one for failure modes, re-runs and security. Their findings are all in this version: the re-run latest problem, the release latest=auto problem, failing closed on missing tags, fail-fast and artifact retention. They also checked that every run: block reads only environment variables (no ${{ }} interpolation), that packages: write is granted only to the two jobs that push, and that no job output carries a secret.

  • I ran the workflow by hand on this PR's branch (run 40678304438). On a branch other than main that is a dry run, and it passed:

    Job Runner architecture Time What ran
    Prepare tags x86_64 4s No tags (not the default branch); "Refuse a publishing run without tags" skipped, since this was a manual run
    Build (amd64) x86_64 1m43s Built with push=false; registry login, digest export and upload skipped
    Build (arm64) aarch64 (native) 2m14s Same. bun install and bun run build ran natively; the build stage took about 40s, against about 140s under QEMU
    Merge and tag — — Skipped (publish false)

    Nothing was pushed to GHCR.

  • On merge, I'll watch the first main run and confirm that it pushes the sha tag and latest as a two-platform image.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • My changes do not introduce new warnings

…talling Under QEMU

The publish workflow built linux/amd64 and linux/arm64 in one job on an
amd64 runner, emulating arm64 with QEMU. bun install stalls there: one run
sat in "Build and push" for over two hours, and the next one's arm64
`bun install --frozen-lockfile` went silent for 13 minutes (848.7s, against
26.5s the run before, on the same BuildKit and binfmt images).

Each architecture now builds on a runner of that architecture
(ubuntu-24.04-arm for arm64, free for public repositories) and pushes an
untagged digest; a merge job then joins the two digests into the tagged
multi-platform image with `docker buildx imagetools create`, as
LibreChat's own publish workflow does. The merge refuses to publish
unless both architectures built (exactly two digests), and a failing leg
no longer cancels the other one, so "Re-run failed jobs" has the healthy
leg's digest to reuse (kept for 7 days).

`latest` is now decided in the merge job, right before it is pushed. The
check is the same (this commit is on main and no later commit changed the
build context), but every re-run of the merge repeats it: GitHub reuses a
successful job's outputs when failed jobs are re-run, so a decision taken
at the start of the run could move `latest` back after newer images
shipped. Releases no longer tag `latest` either: metadata-action's default
`flavor: latest=auto` added it to every semver release, skipping the
check; `latest=false` leaves the merge as its only source.

A publishing run that computes no tags now fails instead of pushing
nothing (a release tag that isn't semver, say), and a manual run on a
branch other than main builds both architectures without pushing, as a
dry run. The image's `org.opencontainers.image.version` label is now the
commit sha on main (it was `latest`, a tag that moves) and the version on
releases, as before.
@danny-avila

Copy link
Copy Markdown
Contributor Author

@codex review

Pass 1. Head: ef9cca9

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T11:16:14.458911Z ef9cca9 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: ef9cca93c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

1 participant