Skip to content

Commit 1959e3e

Browse files
depollCopilot
andcommitted
Fix Docker runner architecture enforcement
Ensure requested Docker runner architecture is actually used by validating pulled/tagged image architecture and forcing container platform during create. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1372429 commit 1959e3e

2 files changed

Lines changed: 46 additions & 6 deletions

File tree

backend/src/services/dockerRunner.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { resolveCredentialById, createClientFromCredentialId } from './credentia
99

1010
// Docker client
1111
let docker: Docker | null = null;
12+
export type DockerArch = 'amd64' | 'arm64';
1213

1314
// Runner image name
1415
const RUNNER_IMAGE = process.env.RUNNER_IMAGE || 'myoung34/github-runner:latest';
@@ -63,13 +64,28 @@ export async function getDockerInfo(): Promise<object | null> {
6364
}
6465
}
6566

67+
export function normalizeImageArchitecture(architecture: string): string {
68+
if (architecture === 'aarch64') return 'arm64';
69+
if (architecture === 'x86_64') return 'amd64';
70+
return architecture;
71+
}
72+
73+
export function assertImageArchitecture(imageRef: string, actualArchitecture: string, expectedArchitecture: DockerArch): void {
74+
const normalized = normalizeImageArchitecture(actualArchitecture);
75+
if (normalized !== expectedArchitecture) {
76+
throw new Error(
77+
`Image ${imageRef} architecture mismatch: expected ${expectedArchitecture}, got ${actualArchitecture}`
78+
);
79+
}
80+
}
81+
6682
/**
6783
* Pull the runner image for the specified platform
6884
* Note: Docker multi-arch images require explicit platform handling.
6985
* We pull with --platform and verify the architecture before use.
7086
*/
7187
export async function pullRunnerImage(
72-
architecture: 'amd64' | 'arm64' = 'amd64'
88+
architecture: DockerArch = 'amd64'
7389
): Promise<string> {
7490
const d = initDocker();
7591
const platform = `linux/${architecture}`;
@@ -90,14 +106,13 @@ export async function pullRunnerImage(
90106
// Check if we already have the platform-specific tag with correct architecture
91107
try {
92108
const existingImage = await d.getImage(platformTag).inspect();
93-
const imageArch = existingImage.Architecture;
94-
const normalizedImageArch = imageArch === 'aarch64' ? 'arm64' : imageArch;
109+
const imageArch = normalizeImageArchitecture(existingImage.Architecture);
95110

96-
if (normalizedImageArch === architecture) {
97-
console.log(`Image ${platformTag} already exists with correct architecture (${imageArch})`);
111+
if (imageArch === architecture) {
112+
console.log(`Image ${platformTag} already exists with correct architecture (${existingImage.Architecture})`);
98113
return platformTag;
99114
}
100-
console.log(`Image ${platformTag} exists but has wrong architecture (${imageArch}), re-pulling...`);
115+
console.log(`Image ${platformTag} exists but has wrong architecture (${existingImage.Architecture}), re-pulling...`);
101116
} catch {
102117
// Image doesn't exist, need to pull
103118
}
@@ -129,6 +144,7 @@ export async function pullRunnerImage(
129144
// Verify the pulled image has the correct architecture
130145
const pulledImage = await d.getImage(RUNNER_IMAGE).inspect();
131146
console.log(`Pulled image architecture: ${pulledImage.Architecture}`);
147+
assertImageArchitecture(RUNNER_IMAGE, pulledImage.Architecture, architecture);
132148

133149
// Tag the pulled image with architecture-specific tag
134150
console.log(`Tagging image as ${platformTag}...`);
@@ -138,6 +154,7 @@ export async function pullRunnerImage(
138154
// Verify the tagged image
139155
const taggedImage = await d.getImage(platformTag).inspect();
140156
console.log(`Tagged image ${platformTag} architecture: ${taggedImage.Architecture}`);
157+
assertImageArchitecture(platformTag, taggedImage.Architecture, architecture);
141158

142159
return platformTag;
143160
}
@@ -171,6 +188,7 @@ export async function createDockerRunner(
171188
if (dockerArch !== 'amd64' && dockerArch !== 'arm64') {
172189
throw new Error(`Unsupported Docker architecture: ${architecture}`);
173190
}
191+
const platform = `linux/${dockerArch}`;
174192

175193
// Resolve credential and get token
176194
const resolved = await resolveCredentialById(credentialId);
@@ -247,6 +265,7 @@ export async function createDockerRunner(
247265
// Create container using the architecture-specific image tag
248266
const container = await d.createContainer({
249267
Image: imageTag,
268+
platform,
250269
name: `action-packer-${runnerId}`,
251270
Env: env,
252271
HostConfig: hostConfig,

backend/tests/dockerRunner.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { normalizeImageArchitecture, assertImageArchitecture } from '../src/services/dockerRunner.js';
3+
4+
describe('dockerRunner architecture checks', () => {
5+
it('normalizes Docker architecture aliases', () => {
6+
expect(normalizeImageArchitecture('aarch64')).toBe('arm64');
7+
expect(normalizeImageArchitecture('x86_64')).toBe('amd64');
8+
expect(normalizeImageArchitecture('amd64')).toBe('amd64');
9+
});
10+
11+
it('accepts matching architecture', () => {
12+
expect(() => assertImageArchitecture('example:tag', 'x86_64', 'amd64')).not.toThrow();
13+
expect(() => assertImageArchitecture('example:tag', 'aarch64', 'arm64')).not.toThrow();
14+
});
15+
16+
it('throws on architecture mismatch', () => {
17+
expect(() => assertImageArchitecture('example:tag', 'arm64', 'amd64')).toThrow(
18+
'Image example:tag architecture mismatch: expected amd64, got arm64'
19+
);
20+
});
21+
});

0 commit comments

Comments
 (0)