Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
17fd367
feat: add native ARM matrix build support
tgenov Feb 25, 2026
569a443
chore: include bundled dist for GitHub Actions runtime
tgenov Feb 25, 2026
b85ad8e
fix: use type=docker output for single-platform builds with platformTag
tgenov Feb 25, 2026
3e2ff32
fix: rebuild dist bundle after type=docker fix
tgenov Feb 25, 2026
98082f9
fix: skip --platform and --output for platformTag builds
tgenov Feb 25, 2026
c959376
feat: add Azure DevOps parity, tests, and docs for platformTag/mergeTag
tgenov Feb 26, 2026
1772e73
fix: restore upstream comments removed during feature implementation
tgenov Feb 26, 2026
7aca3ea
fix: remove dead buildxOutput assignment for platformTag builds
tgenov Mar 28, 2026
48a45ae
fix: validate platformTags input and include stderr in error messages
tgenov Mar 28, 2026
ae3132e
fix: apply push gating to mergeTag manifest creation
tgenov Mar 28, 2026
1d4e181
refactor: rename createManifest to createMultiPlatformImage for clarity
tgenov Mar 28, 2026
cdf98e0
docs: fix Azure DevOps arm64 example to use ARM64 agent pool
tgenov Mar 28, 2026
8eed5ab
refactor: DRY shared platformTag/mergeTag logic into common/src/platf…
tgenov Mar 28, 2026
a7645d9
fix: complete rename in tests and rebuild dist bundle
tgenov Mar 28, 2026
55dea7c
test: add unit tests for common/src/platform.ts helpers
tgenov Mar 28, 2026
655be62
chore: apply prettier formatting and rebuild dist bundles
tgenov Mar 28, 2026
f907cd8
fix: require push=always for mergeTag, validate mutual exclusion with…
tgenov Mar 28, 2026
c397a55
fix: remove duplicate log, update docs for accuracy and completeness
tgenov Mar 28, 2026
04bf6ae
fix: validate buildx and imageName early for mergeTag, guard empty im…
tgenov Mar 28, 2026
413c2df
Update docs/azure-devops-task.md
tgenov Mar 28, 2026
f8180a7
Update docs/github-action.md
tgenov Mar 28, 2026
599ca50
fix: trim and validate platformTag, remove misleading platform from e…
tgenov Mar 28, 2026
303b838
Update github-action/src/main.ts
tgenov Mar 28, 2026
34c8889
Update azdo-task/DevcontainersCi/src/main.ts
tgenov Mar 28, 2026
eb4533e
fix: remove committed dist, fix AzDO doc copy-paste, merge redundant …
tgenov Apr 11, 2026
672b4f9
feat: add platformToTagSuffix, update mergeMultiPlatformImages to acc…
tgenov Apr 13, 2026
31e468b
feat: create merge GitHub Action for multi-arch manifest creation
tgenov Apr 13, 2026
e86d56b
refactor: replace platformTag/mergeTag with useNativeRunner boolean
tgenov Apr 13, 2026
03c3f19
feat: create AzDO DevcontainersMerge task for multi-arch manifest cre…
tgenov Apr 14, 2026
1cacf9e
docs: update for useNativeRunner boolean and separate merge action
tgenov Apr 14, 2026
9fe2e94
refactor: rename platformTags to platformSuffixes in createMultiPlatf…
tgenov Apr 14, 2026
8b2131f
test: add empty string edge case for platformToTagSuffix
tgenov Apr 14, 2026
be04e95
fix: cast error to Error in merge action catch block
tgenov Apr 14, 2026
9409cac
refactor: move platformSuffix derivation inside useNativeRunner block
tgenov Apr 14, 2026
4812b4b
chore: add DevcontainersMerge package-lock.json
tgenov Apr 14, 2026
09c1f19
fix: add imageTag defaults to merge action.yml and AzDO task.json
tgenov Apr 14, 2026
be0dfd5
chore: remove unused jsonc-parser dependency from DevcontainersMerge
tgenov Apr 14, 2026
cb5d676
chore: add prettier config to DevcontainersMerge, wire new packages i…
tgenov Apr 14, 2026
8d02c82
chore: add eslint/prettier config and deps to merge packages
tgenov Apr 14, 2026
4f0745c
docs: explain why push: always is required and note that platform-suf…
tgenov Apr 14, 2026
133ac8e
fix: remove double error reporting in merge docker wrappers
tgenov Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ inputs:
cacheTo:
required: false
description: Specify the image to cache the built image to
useNativeRunner:
required: false
default: 'false'
description: 'Set to true for native multi-platform builds. When true, platform must be a single value (e.g., linux/amd64) and the image tag suffix is auto-derived (linux/amd64 becomes linux-amd64). The --platform flag is not passed to devcontainer build, relying on the native runner architecture.'
Comment thread
tgenov marked this conversation as resolved.
outputs:
runCmdOutput:
description: The output of the command specified in the runCmd input
Expand Down
14 changes: 14 additions & 0 deletions azdo-task/DevcontainersCi/src/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,17 @@ export async function pushImage(
return false;
}
}

export async function createMultiPlatformImage(
imageName: string,
tag: string,
platformTags: string[],
): Promise<boolean> {
try {
await docker.createMultiPlatformImage(exec, imageName, tag, platformTags);
return true;
} catch (error) {
task.setResult(task.TaskResult.Failed, `${error}`);
return false;
}
}
63 changes: 54 additions & 9 deletions azdo-task/DevcontainersCi/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ import {
import {isDockerBuildXInstalled, pushImage} from './docker';
import {isSkopeoInstalled, copyImage} from './skopeo';
import {exec} from './exec';
import {
buildImageNames,
platformToTagSuffix,
} from '../../../common/src/platform';

export async function runMain(): Promise<void> {
try {
task.setTaskVariable('hasRunMain', 'true');

const useNativeRunner =
(task.getInput('useNativeRunner') ?? 'false') === 'true';

const buildXInstalled = await isDockerBuildXInstalled();
if (!buildXInstalled) {
console.log(
Expand Down Expand Up @@ -52,7 +60,31 @@ export async function runMain(): Promise<void> {
const skipContainerUserIdUpdate =
(task.getInput('skipContainerUserIdUpdate') ?? 'false') === 'true';

if (platform) {
if (useNativeRunner) {
if (!platform) {
task.setResult(
task.TaskResult.Failed,
'platform is required when useNativeRunner is true',
);
return;
}
if (platform.includes(',')) {
task.setResult(
task.TaskResult.Failed,
`useNativeRunner requires a single platform value, got '${platform}'`,
);
return;
}
}

let platformSuffix: string | undefined;
if (useNativeRunner) {
platformSuffix = platformToTagSuffix(platform!);
task.setTaskVariable('useNativeRunner', 'true');
task.setTaskVariable('platformSuffix', platformSuffix);
}

if (platform && !useNativeRunner) {
const skopeoInstalled = await isSkopeoInstalled();
if (!skopeoInstalled) {
console.log(
Expand All @@ -61,7 +93,10 @@ export async function runMain(): Promise<void> {
return;
}
}
const buildxOutput = platform ? 'type=oci,dest=/tmp/output.tar' : undefined;
let buildxOutput: string | undefined;
if (platform && !useNativeRunner) {
buildxOutput = 'type=oci,dest=/tmp/output.tar';
}

const log = (message: string): void => console.log(message);
const workspaceFolder = path.resolve(checkoutPath, subFolder);
Expand All @@ -70,10 +105,9 @@ export async function runMain(): Promise<void> {

const resolvedImageTag = imageTag ?? 'latest';
const imageTagArray = resolvedImageTag.split(/\s*,\s*/);
const fullImageNameArray: string[] = [];
for (const tag of imageTagArray) {
fullImageNameArray.push(`${imageName}:${tag}`);
}
const fullImageNameArray = imageName
? buildImageNames(imageName, imageTagArray, platformSuffix)
: [];
if (imageName) {
if (fullImageNameArray.length === 1) {
if (!noCache && !cacheFrom.includes(fullImageNameArray[0])) {
Expand All @@ -98,9 +132,9 @@ export async function runMain(): Promise<void> {
workspaceFolder,
configFile,
imageName: fullImageNameArray,
platform,
platform: useNativeRunner ? undefined : platform,
additionalCacheFroms: cacheFrom,
output: buildxOutput,
output: useNativeRunner ? undefined : buildxOutput,
noCache,
cacheTo,
};
Expand Down Expand Up @@ -192,6 +226,9 @@ export async function runPost(): Promise<void> {
const pushOnFailedBuild =
(task.getInput('pushOnFailedBuild') ?? 'false') === 'true';

const useNativeRunner = task.getTaskVariable('useNativeRunner') === 'true';
const platformSuffix = task.getTaskVariable('platformSuffix');

// default to 'never' if not set and no imageName
if (pushOption === 'never' || (!pushOption && !imageName)) {
console.log(`Image push skipped because 'push' is set to '${pushOption}'`);
Expand Down Expand Up @@ -259,8 +296,16 @@ export async function runPost(): Promise<void> {
}
const imageTag = task.getInput('imageTag') ?? 'latest';
const imageTagArray = imageTag.split(/\s*,\s*/);

const platform = task.getInput('platform');
if (platform) {
if (useNativeRunner && platformSuffix) {
for (const tag of imageTagArray) {
console.log(
`Pushing platform image '${imageName}:${tag}-${platformSuffix}'...`,
);
await pushImage(imageName, `${tag}-${platformSuffix}`);
}
} else if (platform) {
for (const tag of imageTagArray) {
console.log(`Copying multiplatform image '${imageName}:${tag}'...`);
const imageSource = `oci-archive:/tmp/output.tar:${tag}`;
Expand Down
7 changes: 7 additions & 0 deletions azdo-task/DevcontainersCi/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@
"type": "multiLine",
"label": "Specify the image to cache the built image to",
"required": false
},
{
"name": "useNativeRunner",
"type": "boolean",
"label": "Set to true for native multi-platform builds. When true, platform must be a single value and the image tag suffix is auto-derived.",
"required": false,
"defaultValue": false
}
],
"outputVariables": [{
Expand Down
4 changes: 4 additions & 0 deletions azdo-task/DevcontainersMerge/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
lib/
node_modules/
jest.config.js
54 changes: 54 additions & 0 deletions azdo-task/DevcontainersMerge/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"plugins": ["jest", "@typescript-eslint"],
"extends": ["plugin:github/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"i18n-text/no-en": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error",
"camelcase": "off",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
"func-call-spacing": ["error", "never"],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"semi": ["error", "always"],
"@typescript-eslint/unbound-method": "error",
"no-console": "off"
},
"env": {
"node": true,
"es6": true,
"jest/globals": true
}
}
3 changes: 3 additions & 0 deletions azdo-task/DevcontainersMerge/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
lib/
node_modules/
10 changes: 10 additions & 0 deletions azdo-task/DevcontainersMerge/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": true,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": false,
"arrowParens": "avoid"
}
Loading