Skip to content

ci(build): build release images natively per arch and merge manifests - #1705

Merged
laboef1900 merged 1 commit into
devfrom
feature/native-arm64-image-builds
Oct 7, 2026
Merged

laboef1900 merged 1 commit into
devfrom
feature/native-arm64-image-builds

Conversation

@laboef1900

Copy link
Copy Markdown
Contributor

Summary

Why. Release (v*) images built linux/arm64 under QEMU on amd64 runners. Emulated arm64 npm ci is unreliable. In the v0.9.1 tag build (run 37678912473), MCP Docs Image hung for more than 1.5 h in #30 [linux/arm64 builder 4/7] RUN npm ci --ignore-scripts on attempt 1 (cancelled 21:39Z) and again on attempt 2 (cancelled 22:31Z). Attempt 3 (22:33–22:35Z) eventually got through. EE hits the same failure as qemu: uncaught target signal 4 (Illegal instruction). Run alone, the same npm ci succeeds, so the problem is the emulation, not a dependency.

What. The workflow now follows Docker's "Distribute build across multiple runners" pattern: each architecture builds natively, pushes by digest, and a merge job combines the digests into one manifest.

  • New reusable .github/workflows/docker-image.yml. Each of the four image jobs in docker-build.yml calls it, which removes about 240 lines of duplicated steps.
    • Branch builds (dev/main): unchanged. One ubuntu-latest amd64 job pushes the metadata tags directly, with the same cache ref (:buildcache) and the same attestation flags (verified from the buildx command lines). The merge job is skipped.
    • Release builds: a matrix runs amd64 on ubuntu-latest and arm64 on ubuntu-24.04-arm.
      • Each leg pushes with outputs: type=image,push-by-digest=true,name-canonical=true,push=true and uploads its digest as an artifact.
      • The merge manifest job runs docker buildx imagetools create with the same metadata-action tags as before ({{version}}, {{major}}.{{minor}}, plus latest via latest=auto).
      • It then asserts the index contains exactly linux/amd64 + linux/arm64 (attestation manifests excluded).
      • QEMU is removed.
    • Backend/frontend/mcp-docs keep sbom: true + provenance: mode=max on releases. SearXNG keeps the buildx defaults. Each per-arch digest is an index (image + attestation manifest), and imagetools create keeps both platforms' SBOM and provenance (verified below).
    • Registry cache is split per architecture: amd64 keeps :buildcache, so branch builds stay warm; arm64 uses :buildcache-arm64.
    • The backend runtime import smoke now runs on every built arch, natively, against the exact digest that was pushed and that the merge indexes. Before, it was skipped on tags.
    • The frontend build-info.json stamping is unchanged.
  • changes job: tag builds and plain workflow_dispatch still build all four images, and push path filtering is unchanged (it now also watches docker-image.yml). It also resolves release_tag, the checkout ref, and the latest flavor.
  • New workflow_dispatch inputs
    • tag (e.g. v0.9.1): checks out refs/tags/<tag> and computes metadata as if building that tag (type=semver,…,value=<tag>; the revision label is the tag's commit). It builds both arches natively and pushes, without moving the git tag, and works when dispatched from dev/main. A rebuild only takes latest if <tag> is the newest stable v* tag, so rebuilding an old release can't roll latest back. The concurrency group includes the tag, so a rebuild doesn't cancel the branch's pushes and isn't cancelled by them.
    • images (default all): a comma-separated subset of backend, frontend, searxng, mcp-docs.
    • Without inputs, dispatch behaves as before.
  • Docs: CLAUDE.md (Versioning → release images + rebuild command), docs/architecture/05-deployment.md, CHANGELOG.
  • backend/src/dockerfile-prod-modules.test.ts now finds the smoke script in the backend job's smoke_test: input. It still requires collab-schema and @tiptap/extension-code-block.

Rebuild a release after merge

gh workflow run docker-build.yml -R Compendiq/compendiq-ce --ref dev -f tag=v0.9.1 -f images=mcp-docs

Use images=all to republish all four images. Note: compendiq-ce-mcp-docs:0.9.1 is already published (index sha256:b19c26b2…, amd64 + arm64 + attestations), from attempt 3 of run 37678912473. A rebuild is optional; see Risks for what it re-tags.

Verification

  • actionlint 1.7.12 (rhysd/actionlint image): 0 errors on both workflows. Both files also pass a YAML parse.
  • The changes job scripts ran in ubuntu:24.04 against a scratch repo with tags v0.9.0, v0.9.1, v0.10.0-rc.1:
    • newest tag gives latest=true; older and prerelease tags give false.
    • bad or missing tags (v9.9.9, 0.9.1, v0.9.1;id) fail.
    • tag push gives latest=auto.
    • images subsets work and unknown names fail.
    • push diffs flag only the touched images; a zero or bad before builds all four.
  • dockerfile-prod-modules.test.ts: 8/8 pass.
  • Real runs from this branch used a temporary scratch_tag input that only replaced the final tags with :nativearm-test / :nativearm-test-branch. That hook commit was dropped before this head, and the runs were deleted afterwards.
    • Run 37697972760 (-f tag=v0.9.1 -f images=all), release path: success, all 13 jobs.
      • arm64 legs ran on ubuntu-24.04-arm (Architecture: aarch64); amd64 legs on ubuntu-latest.
      • The previously hanging step [builder 4/7] RUN npm ci --ignore-scripts finished in 4.7 s natively on arm64 for mcp-docs.
      • Backend smoke runtime require smoke ok passed on both amd64 and arm64.
      • The merge jobs computed :0.9.1 :0.9 :latest for every image, the same as the published v0.9.1 output (:latest == :0.9.1 digest today).
      • Index platforms: ["linux/amd64","linux/arm64"] for all four.
      • Inspecting the merged indexes showed provenance on both platforms for all four images, and an SBOM on both platforms for backend/frontend/mcp-docs (SearXNG has none, as before).
      • org.opencontainers.image.revision = ce05906b… (the v0.9.1 commit, not the branch head), version 0.9.1.
      • Wall time about 2.5 min.
    • Run 37697999607 (no tag, images=all), branch path: success. It ran 4 amd64 jobs, and the merge jobs were skipped.
      • Each image was pushed directly with tags.
      • Backend/frontend/mcp-docs had provenance and SBOM disabled, and SearXNG used the default provenance, the same flags as today.
      • The backend smoke passed and the frontend build-info.json was stamped.
  • Cleanup: deleted 37 GHCR package versions created by those runs: the scratch tags, the per-arch digests and their children, and two superseded cache manifests. All :0.9.1 tags were left untouched. Both test runs were deleted. No v* tag was created, moved or deleted.

Risks

  • Rebuilds re-tag with new digests. Builds are not bit-reproducible: the created label, the frontend builtAt, and SBOM/provenance timestamps all change. Rebuilding an existing tag moves X.Y.Z, X.Y (and latest if it is the newest tag) to new, functionally equivalent digests. The previous digests stay pullable by @sha256. Use images= to limit what gets re-tagged.
  • Release builds now depend on GitHub's ubuntu-24.04-arm runner availability. If that label stops working, the arm64 leg queues or fails and the merge doesn't run, so nothing is tagged. It does not publish an amd64-only release.
  • The first release after merge starts with a cold :buildcache-arm64. The test run already warmed it with v0.9.1 sources, and that cache tag was kept on purpose.
  • A failed leg leaves its successful sibling's digest pushed but untagged, which is harmless.
  • Job names changed to <Image> / build (amd64|arm64) and <Image> / merge manifest. None of these are PR status checks (the workflow runs on push and dispatch only).

Release (v*) images built linux/arm64 under QEMU on amd64 runners. Emulated
arm64 npm ci hangs or dies with 'qemu: uncaught target signal 4 (Illegal
instruction)': in the v0.9.1 tag build (run 37678912473) MCP Docs Image hung
>1.5h in arm64 'npm ci' on attempts 1 and 2 before attempt 3 got through.

Each image now goes through a reusable docker-image.yml: branch builds keep
one amd64 job pushing their tags directly; release builds run amd64 on
ubuntu-latest and arm64 on ubuntu-24.04-arm, push by digest with SBOM +
provenance, and a merge job assembles the index with the unchanged
metadata-action tags and asserts it holds exactly amd64 and arm64. The
backend import smoke now runs natively on every built arch against the
pushed digest. arm64 gets its own registry cache ref.

workflow_dispatch gains 'tag' (rebuild an existing release tag from any
branch without moving it; latest only if it is the newest stable tag) and
'images' (subset to build).
@laboef1900
laboef1900 merged commit 6ddcd3c into dev Oct 7, 2026
8 checks passed
@laboef1900
laboef1900 deleted the feature/native-arm64-image-builds branch October 8, 2026 00:10
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