Skip to content

Commit 68f7a62

Browse files
inistorclaude
andcommitted
feat: support image.docker.platform
Adds support for `image.docker.platform` per GitLab CI spec (introduced in GitLab 18.0). The platform string is forwarded to both: - `docker pull --platform <platform>` to fetch the requested manifest - `docker run --platform <platform>` so the container runs against that platform even if a multi-arch image was already cached locally Mirrors the existing `image.docker.user` plumbing. Refs https://docs.gitlab.com/ci/yaml/#imagedockerplatform — supersedes the abandoned attempt in #1595 (closed for missing tests + style). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eb5a5d9 commit 68f7a62

4 files changed

Lines changed: 49 additions & 5 deletions

File tree

src/data-expander.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ export function imageComplex (data: any) {
188188
return {
189189
name: typeof data === "string" ? data : data.name,
190190
entrypoint: data.entrypoint,
191-
...(data.docker?.user ? {docker: {user: data.docker?.user}} : {}),
191+
...(data.docker?.user || data.docker?.platform ? {docker: {user: data.docker.user, platform: data.docker.platform}} : {}),
192192
};
193193
}
194194

src/job.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,7 @@ If you know what you're doing and would like to suppress this warning, use one o
670670
this._dotenvVariables = await this.initProducerReportsDotenvVariables(writeStreams, Utils.expandVariables(this._variables));
671671
const expanded = Utils.unscape$$Variables(Utils.expandVariables({...this._variables, ...this._dotenvVariables}));
672672
const imageName = this.imageName(expanded);
673+
const imagePlatform = this.imagePlatform(expanded);
673674
const helperImageName = argv.helperImage;
674675
const safeJobName = this.safeJobName;
675676

@@ -682,7 +683,7 @@ If you know what you're doing and would like to suppress this warning, use one o
682683
}
683684

684685
if (imageName) {
685-
await this.pullImage(writeStreams, imageName);
686+
await this.pullImage(writeStreams, imageName, imagePlatform);
686687

687688
const buildVolumeName = this.buildVolumeName;
688689
const tmpVolumeName = this.tmpVolumeName;
@@ -971,6 +972,11 @@ If you know what you're doing and would like to suppress this warning, use one o
971972
dockerCmd += `--user ${imageUser} `;
972973
}
973974

975+
const imagePlatform = this.imagePlatform(expanded);
976+
if (imagePlatform) {
977+
dockerCmd += `--platform ${imagePlatform} `;
978+
}
979+
974980
if (this.argv.containerEmulate) {
975981
const runnerName: string = this.argv.containerEmulate;
976982

@@ -1207,6 +1213,13 @@ If you know what you're doing and would like to suppress this warning, use one o
12071213
return Utils.expandText(image["docker"]["user"], vars);
12081214
}
12091215

1216+
private imagePlatform (vars: {[key: string]: string} = {}): string | null {
1217+
const image = this.jobData["image"];
1218+
if (!image) return null;
1219+
if (!image["docker"]) return null;
1220+
return Utils.expandText(image["docker"]["platform"], vars);
1221+
}
1222+
12101223
get imageEntrypoint (): string[] | null {
12111224
const image = this.jobData["image"];
12121225

@@ -1235,14 +1248,16 @@ If you know what you're doing and would like to suppress this warning, use one o
12351248
}
12361249
}
12371250

1238-
private async pullImage (writeStreams: WriteStreams, imageToPull: string) {
1251+
private async pullImage (writeStreams: WriteStreams, imageToPull: string, imagePlatform: string | null = null) {
12391252
const pullPolicy = this.argv.pullPolicy;
1253+
const platformArgs = imagePlatform ? ["--platform", imagePlatform] : [];
1254+
const platformSuffix = imagePlatform ? ` (${imagePlatform})` : "";
12401255
const actualPull = async () => {
12411256
await this.validateCiDependencyProxyServerAuthentication(imageToPull);
12421257
const time = process.hrtime();
1243-
await Utils.spawn([this.argv.containerExecutable, "pull", imageToPull]);
1258+
await Utils.spawn([this.argv.containerExecutable, "pull", imageToPull, ...platformArgs]);
12441259
const endTime = process.hrtime(time);
1245-
writeStreams.stdout(chalk`${this.formattedJobName} {magentaBright pulled} ${imageToPull} in {magenta ${prettyHrtime(endTime)}}\n`);
1260+
writeStreams.stdout(chalk`${this.formattedJobName} {magentaBright pulled} ${imageToPull}${platformSuffix} in {magenta ${prettyHrtime(endTime)}}\n`);
12461261
this.refreshLongRunningSilentTimeout(writeStreams);
12471262
};
12481263

tests/test-cases/image/.gitlab-ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ image-user:
6464
script:
6565
- id -u
6666

67+
image-platform:
68+
image:
69+
name: alpine
70+
docker:
71+
platform: linux/amd64
72+
script:
73+
- uname -m
74+
6775
image-entrypoint-with-variables:
6876
variables:
6977
FOO: BAR

tests/test-cases/image/integration.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,27 @@ test.concurrent("image <image-user>", async () => {
128128
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
129129
});
130130

131+
test.concurrent("image <image-platform>", async () => {
132+
const writeStreams = new WriteStreamsMock();
133+
134+
await handler({
135+
pullPolicy: "always",
136+
cwd: "tests/test-cases/image",
137+
job: ["image-platform"],
138+
stateDir: ".gitlab-ci-local-image-platform",
139+
}, writeStreams);
140+
141+
// The "pulled" log line includes the requested platform, proving --platform
142+
// was forwarded to `docker pull` rather than silently dropped. Use `.*` to
143+
// span the chalk ANSI escape codes between tokens.
144+
expect(writeStreams.stdoutLines.join("\n")).toMatch(/pulled.*alpine.*linux\/amd64/);
145+
146+
// The job runs successfully on the host (linux/amd64 matches the CI runner arch).
147+
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining([
148+
chalk`{blueBright image-platform} {greenBright >} x86_64`,
149+
]));
150+
});
151+
131152
test.concurrent("pull invalid image", async () => {
132153
const jobs: Job[] = [];
133154
const writeStreams = new WriteStreamsMock();

0 commit comments

Comments
 (0)