Skip to content

Commit 00115d9

Browse files
authored
Merge pull request #435 from tgenov/main
feat: native multi-platform builds via runner matrix (no QEMU emulation)
2 parents ec57048 + f2c9645 commit 00115d9

40 files changed

Lines changed: 10804 additions & 29 deletions

.github/workflows/ci_common.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ jobs:
292292
run: |
293293
(cd common && npm install && npm run build)
294294
(cd github-action/ && npm install && npm run build && npm run package)
295+
(cd merge/ && npm install && npm run build && npm run package)
295296
296297
# - name: Publish AzDO Task
297298
# uses: ./

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ inputs:
6666
cacheTo:
6767
required: false
6868
description: Specify the image to cache the built image to
69+
useNativeRunner:
70+
required: false
71+
default: 'false'
72+
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.'
6973
outputs:
7074
runCmdOutput:
7175
description: The output of the command specified in the runCmd input

azdo-task/DevcontainersCi/src/main.ts

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,18 @@ import {
1212
import {isDockerBuildXInstalled, pushImage} from './docker';
1313
import {isSkopeoInstalled, copyImage} from './skopeo';
1414
import {exec} from './exec';
15+
import {
16+
buildImageNames,
17+
platformToTagSuffix,
18+
} from '../../../common/src/platform';
1519

1620
export async function runMain(): Promise<void> {
1721
try {
1822
task.setTaskVariable('hasRunMain', 'true');
23+
24+
const useNativeRunner =
25+
(task.getInput('useNativeRunner') ?? 'false') === 'true';
26+
1927
const buildXInstalled = await isDockerBuildXInstalled();
2028
if (!buildXInstalled) {
2129
console.log(
@@ -52,7 +60,31 @@ export async function runMain(): Promise<void> {
5260
const skipContainerUserIdUpdate =
5361
(task.getInput('skipContainerUserIdUpdate') ?? 'false') === 'true';
5462

55-
if (platform) {
63+
if (useNativeRunner) {
64+
if (!platform) {
65+
task.setResult(
66+
task.TaskResult.Failed,
67+
'platform is required when useNativeRunner is true',
68+
);
69+
return;
70+
}
71+
if (platform.includes(',')) {
72+
task.setResult(
73+
task.TaskResult.Failed,
74+
`useNativeRunner requires a single platform value, got '${platform}'`,
75+
);
76+
return;
77+
}
78+
}
79+
80+
let platformSuffix: string | undefined;
81+
if (useNativeRunner) {
82+
platformSuffix = platformToTagSuffix(platform!);
83+
task.setTaskVariable('useNativeRunner', 'true');
84+
task.setTaskVariable('platformSuffix', platformSuffix);
85+
}
86+
87+
if (platform && !useNativeRunner) {
5688
const skopeoInstalled = await isSkopeoInstalled();
5789
if (!skopeoInstalled) {
5890
console.log(
@@ -61,7 +93,10 @@ export async function runMain(): Promise<void> {
6193
return;
6294
}
6395
}
64-
const buildxOutput = platform ? 'type=oci,dest=/tmp/output.tar' : undefined;
96+
let buildxOutput: string | undefined;
97+
if (platform && !useNativeRunner) {
98+
buildxOutput = 'type=oci,dest=/tmp/output.tar';
99+
}
65100

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

71106
const resolvedImageTag = imageTag ?? 'latest';
72107
const imageTagArray = resolvedImageTag.split(/\s*,\s*/);
73-
const fullImageNameArray: string[] = [];
74-
for (const tag of imageTagArray) {
75-
fullImageNameArray.push(`${imageName}:${tag}`);
76-
}
108+
const fullImageNameArray = imageName
109+
? buildImageNames(imageName, imageTagArray, platformSuffix)
110+
: [];
77111
if (imageName) {
78112
if (fullImageNameArray.length === 1) {
79113
if (!noCache && !cacheFrom.includes(fullImageNameArray[0])) {
@@ -98,9 +132,9 @@ export async function runMain(): Promise<void> {
98132
workspaceFolder,
99133
configFile,
100134
imageName: fullImageNameArray,
101-
platform,
135+
platform: useNativeRunner ? undefined : platform,
102136
additionalCacheFroms: cacheFrom,
103-
output: buildxOutput,
137+
output: useNativeRunner ? undefined : buildxOutput,
104138
noCache,
105139
cacheTo,
106140
};
@@ -192,6 +226,9 @@ export async function runPost(): Promise<void> {
192226
const pushOnFailedBuild =
193227
(task.getInput('pushOnFailedBuild') ?? 'false') === 'true';
194228

229+
const useNativeRunner = task.getTaskVariable('useNativeRunner') === 'true';
230+
const platformSuffix = task.getTaskVariable('platformSuffix');
231+
195232
// default to 'never' if not set and no imageName
196233
if (pushOption === 'never' || (!pushOption && !imageName)) {
197234
console.log(`Image push skipped because 'push' is set to '${pushOption}'`);
@@ -259,8 +296,16 @@ export async function runPost(): Promise<void> {
259296
}
260297
const imageTag = task.getInput('imageTag') ?? 'latest';
261298
const imageTagArray = imageTag.split(/\s*,\s*/);
299+
262300
const platform = task.getInput('platform');
263-
if (platform) {
301+
if (useNativeRunner && platformSuffix) {
302+
for (const tag of imageTagArray) {
303+
console.log(
304+
`Pushing platform image '${imageName}:${tag}-${platformSuffix}'...`,
305+
);
306+
await pushImage(imageName, `${tag}-${platformSuffix}`);
307+
}
308+
} else if (platform) {
264309
for (const tag of imageTagArray) {
265310
console.log(`Copying multiplatform image '${imageName}:${tag}'...`);
266311
const imageSource = `oci-archive:/tmp/output.tar:${tag}`;

azdo-task/DevcontainersCi/task.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@
128128
"type": "multiLine",
129129
"label": "Specify the image to cache the built image to",
130130
"required": false
131+
},
132+
{
133+
"name": "useNativeRunner",
134+
"type": "boolean",
135+
"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.",
136+
"required": false,
137+
"defaultValue": false
131138
}
132139
],
133140
"outputVariables": [{
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dist/
2+
lib/
3+
node_modules/
4+
jest.config.js
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"plugins": ["jest", "@typescript-eslint"],
3+
"extends": ["plugin:github/recommended"],
4+
"parser": "@typescript-eslint/parser",
5+
"parserOptions": {
6+
"ecmaVersion": 9,
7+
"sourceType": "module",
8+
"project": "./tsconfig.json"
9+
},
10+
"rules": {
11+
"eslint-comments/no-use": "off",
12+
"import/no-namespace": "off",
13+
"i18n-text/no-en": "off",
14+
"no-unused-vars": "off",
15+
"@typescript-eslint/no-unused-vars": "error",
16+
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
17+
"@typescript-eslint/no-require-imports": "error",
18+
"@typescript-eslint/array-type": "error",
19+
"@typescript-eslint/await-thenable": "error",
20+
"@typescript-eslint/ban-ts-comment": "error",
21+
"camelcase": "off",
22+
"@typescript-eslint/consistent-type-assertions": "error",
23+
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
24+
"func-call-spacing": ["error", "never"],
25+
"@typescript-eslint/no-array-constructor": "error",
26+
"@typescript-eslint/no-empty-interface": "error",
27+
"@typescript-eslint/no-explicit-any": "off",
28+
"@typescript-eslint/no-extraneous-class": "error",
29+
"@typescript-eslint/no-for-in-array": "error",
30+
"@typescript-eslint/no-inferrable-types": "error",
31+
"@typescript-eslint/no-misused-new": "error",
32+
"@typescript-eslint/no-namespace": "error",
33+
"@typescript-eslint/no-non-null-assertion": "warn",
34+
"@typescript-eslint/no-unnecessary-qualifier": "error",
35+
"@typescript-eslint/no-unnecessary-type-assertion": "error",
36+
"@typescript-eslint/no-useless-constructor": "error",
37+
"@typescript-eslint/no-var-requires": "error",
38+
"@typescript-eslint/prefer-for-of": "warn",
39+
"@typescript-eslint/prefer-function-type": "warn",
40+
"@typescript-eslint/prefer-includes": "error",
41+
"@typescript-eslint/prefer-string-starts-ends-with": "error",
42+
"@typescript-eslint/promise-function-async": "error",
43+
"@typescript-eslint/require-array-sort-compare": "error",
44+
"@typescript-eslint/restrict-plus-operands": "error",
45+
"semi": ["error", "always"],
46+
"@typescript-eslint/unbound-method": "error",
47+
"no-console": "off"
48+
},
49+
"env": {
50+
"node": true,
51+
"es6": true,
52+
"jest/globals": true
53+
}
54+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dist/
2+
lib/
3+
node_modules/
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"printWidth": 80,
3+
"tabWidth": 2,
4+
"useTabs": true,
5+
"semi": true,
6+
"singleQuote": true,
7+
"trailingComma": "all",
8+
"bracketSpacing": false,
9+
"arrowParens": "avoid"
10+
}

0 commit comments

Comments
 (0)