feat: native multi-platform builds via runner matrix (no QEMU emulation)#435
feat: native multi-platform builds via runner matrix (no QEMU emulation)#435tgenov wants to merge 41 commits intodevcontainers:mainfrom
Conversation
Add platformTag and mergeTag inputs to support building on native ARM runners in a matrix strategy, then merging per-platform images into a multi-arch manifest via docker buildx imagetools create. This avoids slow QEMU emulation for multi-platform builds by allowing each matrix job to build natively for its own platform.
The devcontainer CLI rejects --platform without --output. For native single-platform builds (platformTag set), use type=docker to load the image into the local daemon for subsequent docker push.
The devcontainer CLI rejects --platform for docker-compose-based devcontainers. When platformTag is set, the runner is already the correct native architecture, so --platform is unnecessary.
- Mirror platformTag/mergeTag logic in azdo-task (task.json inputs, runMain/runPost in main.ts, createManifest wrapper in docker.ts) - Add unit tests for createManifest in common/__tests__/docker.test.ts - Update docs/github-action.md and docs/azure-devops-task.md input tables - Add native multi-platform builds section to docs/multi-platform-builds.md with examples for both GitHub Actions and Azure DevOps Pipelines
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Pull request overview
Adds a native (non-QEMU) multi-platform build strategy by splitting per-architecture builds into matrix jobs that push platform-suffixed tags, followed by a final manifest-merge job that publishes a multi-arch tag.
Changes:
- Introduces
platformTag(per-platform tagging/push) andmergeTag(manifest merge) flows for both GitHub Actions and Azure DevOps. - Adds a
createManifestimplementation usingdocker buildx imagetools create, plus unit tests. - Updates docs and action/task metadata to document and expose the new inputs.
Reviewed changes
Copilot reviewed 11 out of 15 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
action.yml |
Exposes new platformTag / mergeTag inputs for the GitHub Action. |
github-action/src/main.ts |
Implements platform-suffixed tagging, merge-only early return, and post-step manifest creation/push behavior. |
github-action/src/docker.ts |
Adds a GitHub Action wrapper for createManifest. |
github-action/dist/sourcemap-register.js |
Updated build artifact. |
github-action/dist/licenses.txt |
Updated build artifact. |
common/src/docker.ts |
Adds shared createManifest implementation via docker buildx imagetools create. |
common/__tests__/docker.test.ts |
Adds unit tests for createManifest. |
azdo-task/DevcontainersCi/task.json |
Exposes new platformTag / mergeTag inputs for the AzDO task. |
azdo-task/DevcontainersCi/src/main.ts |
Mirrors the platform-suffixed tagging and manifest merge flow in AzDO. |
azdo-task/DevcontainersCi/src/docker.ts |
Adds an AzDO wrapper for createManifest. |
docs/multi-platform-builds.md |
Documents the new native matrix strategy and examples. |
docs/github-action.md |
Documents new GitHub Action inputs. |
docs/azure-devops-task.md |
Documents new AzDO task inputs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "required": false | ||
| }, | ||
| { | ||
| "name": "mergeTag", |
There was a problem hiding this comment.
Nit: Naming could be more clear
There was a problem hiding this comment.
Could you clarify what naming you'd prefer? Happy to rename.
There was a problem hiding this comment.
Perhaps mergePlatformTags?
|
@tgenov Thanks for kickstarting this work, it would be very useful to have native builds. I'll be happy to re-review once the above comments are addressed. |
Move the mergeTag block after the push option filtering logic in both GitHub Action and Azure DevOps implementations. Previously, mergeTag would bypass all push gating and could publish manifests on PRs or when push was set to 'never'.
…orm.ts Extract buildImageNames and mergeMultiPlatformImages helpers to eliminate duplicated logic between GitHub Action and Azure DevOps implementations.
… platformTag - Fail early with a clear error if mergeTag is set without push: always, preventing silent no-ops when default push filtering skips the manifest. - Fail early if both mergeTag and platformTag are set on the same step. - Simplify redundant return logic in mergeTag runPost blocks. - Add push: always to manifest job examples in docs.
- Remove duplicate "Creating multi-arch manifest" log from AzDO wrapper (mergeMultiPlatformImages already logs this). - Shorten GH Action group header to avoid repeating the log message. - Update General Notes to reflect GitHub's hosted ARM runners and link to the native matrix strategy. - Add missing Docker login and buildx setup steps to the AzDO native multi-platform example.
|
Thanks for the review. All (but one) comments addressed. Beyond the requested changes, I also added validation that mergeTag and platformTag cannot be set together, and updated the docs General Notes section to reflect GitHub's hosted ARM runners. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…test - Remove github-action/dist/ from tracking (upstream compiles in CI per PR devcontainers#210) - Fix "GitHub Actions Runner" -> "Azure Pipelines agent" in azure-devops-task.md - Merge redundant "returns true" test into the existing happy-path test
|
EDIT: Previous version of the comment was way too verbose for this stage of the process. You're right that this is a lot to track across the two platforms. Both questions connect to the open question in the PR description about polymorphism vs. separation. On dropping On separating the merge: agreed, that would help. I'd propose extracting the merge step into a new Want me to go ahead with that, or do you have a different split in mind? |
|
@tgenov Thank you for thinking this through in detail.
Could we instead introduce a Some checks we should add:
That would be great. The merge action should ideally take platforms in the standard format (e.g. |
…ept standard platform format - platformToTagSuffix converts 'linux/amd64' to 'linux-amd64' - mergeMultiPlatformImages now takes platforms in standard format (e.g., 'linux/amd64,linux/arm64') and auto-derives tag suffixes - Updated and expanded tests
|
@abdurriq Quick clarification on the With native builds, An alternative: when The downside of auto-detection is that the build job becomes implicit about its platform while the merge action still needs an explicit Happy to go either way -- just want to flag the ambiguity before building on it. |
|
To be clear on the auto-detection risk: So re-using |
|
@tgenov I see, in that case let's change the semantics. |
Eliminates the non-null assertion on platformSuffix when saving state, since the value is now computed where platform is guaranteed to be set.
Both merge/ and azdo-task/DevcontainersMerge/ now have eslint and prettier configuration matching their sibling packages. The npm run all script includes format and lint steps. All source files pass both checks.
…fixed tags persist
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 39 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
merge/package.json:1
- The
packagescript invokesncc buildwithout an entry file, which will causenpm run package(and thereforenpm run all) to fail. Update the script to pass the correct entrypoint (e.g., the compiled main file) and ensure the output matchesrun-main.jsexpectations (typicallydist/index.js).
merge/src/main.ts:1 - This overwrites more specific failure details that can already be emitted by the underlying
createMultiPlatformImagewrapper (which callscore.setFailed(error)on exceptions). Consider propagating/returning the original error and setting a single, specific failure message here (or avoid callingsetFailedin the lower-level wrapper sorunMaincan surface the real error).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
createMultiPlatformImage was calling setResult/setFailed with the detailed error, then main.ts overwrote it with a generic message. Now docker.ts logs the detail and main.ts is the single source of truth for task failure status.
|
PR description has been updated to reflect the current state of the commit (the original implementation was superceded based on feedback). |
Problem
The current multi-platform build support (
platforminput) relies on QEMU emulation through Docker buildx. This works but has significant drawbacks:GitHub Actions and Azure DevOps both offer native ARM runners (
ubuntu-24.04-arm,ARM64pool), making emulation unnecessary if the action supports a matrix-based workflow where each platform builds natively on its own runner.Solution
The implementation uses a clean separation of concerns:
useNativeRunnerboolean on the existing build action/task — signals that the build is running on a native runner for a single platform. Whentrue,platformmust be a single value (e.g.,linux/amd64) and the image tag suffix is auto-derived (linux/amd64→linux-amd64). The--platformflag is not passed to devcontainer build, relying on the native runner architecture.Dedicated merge action/task — a separate
devcontainers/ci/mergeGitHub Action andDevcontainersMerge@0Azure DevOps task that combines per-platform images into a multi-arch manifest usingdocker buildx imagetools create.Example Workflow (GitHub Actions)
Scope of Work
GitHub Action (
action.yml)useNativeRunnerboolean inputuseNativeRunner=true: validate single platform, auto-derive tag suffix viaplatformToTagSuffix(), skip--platformflag, push platform-suffixed imagesGitHub Action — Merge (
merge/action.yml)imageName,imageTag,platformsinputsdocker buildx imagetools createto combine per-platform imagesAzure DevOps Task (
DevcontainersCi)useNativeRunnerboolean inputAzure DevOps Task — Merge (
DevcontainersMerge)vss-extension.jsonCommon
platformToTagSuffix()tocommon/src/platform.ts— convertslinux/amd64→linux-amd64mergeMultiPlatformImages()to accept standard platform format and auto-derive suffixescreateMultiPlatformImageincommon/src/docker.ts— validates/trims input, includes stderr/stdout in errorsTests
platformToTagSuffix,buildImageNames,mergeMultiPlatformImages(16 tests)createMultiPlatformImage(7 tests)Documentation
docs/github-action.mdwithuseNativeRunnerinput + merge action referencedocs/azure-devops-task.mdwithuseNativeRunnerinput + merge task referencedocs/multi-platform-builds.mdwith "How it works" section, revised examples for both platformsDesign Notes
Why
useNativeRunnerinstead of extendingplatform?The existing
platforminput (e.g.,linux/amd64,linux/arm64) triggers a single QEMU-emulated build.useNativeRunneris a mode signal that says "this is a native build, skip--platform" — it can't be folded intoplatformbecauseplatform: linux/amd64already means "use QEMU via buildx" for existing users. The tag suffix is auto-derived from the platform value, so users don't need to manually compute it.Why a separate merge action/task?
The original approach overloaded the build action with merge logic via
mergeTag, adding conditional complexity to bothrunMain()andrunPost(). Extracting the merge into dedicated actions keeps each action's contract simple and avoids the push-gating concerns that arose in review.Why
docker buildx imagetools createinstead ofdocker manifest?imagetools createworks with remote registry images without pulling them locally, and supports OCI image indexes natively. It is the recommended approach for combining multi-platform images that are already pushed to a registry.Backwards compatibility
When
useNativeRunnerisfalse(default), the action behaves exactly as before. The existingplatforminput with QEMU emulation continues to work unchanged.platformonlyplatform+useNativeRunner: trueplatformsNote
We are already using the fork internally with GitHub Actions. The DevOps task implementation follows the same patterns but has not been tested in an AzDO pipeline.