Skip to content

Commit cc32133

Browse files
authored
feat: add build-tools to SHA digest pinning infrastructure (#5986)
1 parent f0898b2 commit cc32133

5 files changed

Lines changed: 59 additions & 22 deletions

File tree

src/image-tag.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from 'path';
22
import { parseImageTag, buildRuntimeImageRef, assignImageSource } from './image-tag';
33

4-
const IMAGE_DIGEST_KEYS = ['squid', 'agent', 'agent-act', 'api-proxy', 'cli-proxy'] as const;
4+
const IMAGE_DIGEST_KEYS = ['squid', 'agent', 'agent-act', 'api-proxy', 'cli-proxy', 'build-tools'] as const;
55

66
const VALID_DIGEST = 'sha256:' + 'a'.repeat(64);
77

@@ -135,6 +135,19 @@ describe('buildRuntimeImageRef', () => {
135135
const ref = buildRuntimeImageRef('ghcr.io/github/gh-aw-firewall', 'agent', parsed);
136136
expect(ref).toBe('ghcr.io/github/gh-aw-firewall/agent:0.25.18');
137137
});
138+
139+
it('should build ref for build-tools image with digest', () => {
140+
const buildToolsDigest = 'sha256:' + 'd'.repeat(64);
141+
const parsed = parseImageTag(`0.28.0,build-tools=${buildToolsDigest}`);
142+
const ref = buildRuntimeImageRef('ghcr.io/github/gh-aw-firewall', 'build-tools', parsed);
143+
expect(ref).toBe(`ghcr.io/github/gh-aw-firewall/build-tools:0.28.0@${buildToolsDigest}`);
144+
});
145+
146+
it('should build ref for build-tools image without digest', () => {
147+
const parsed = parseImageTag('0.28.0');
148+
const ref = buildRuntimeImageRef('ghcr.io/github/gh-aw-firewall', 'build-tools', parsed);
149+
expect(ref).toBe('ghcr.io/github/gh-aw-firewall/build-tools:0.28.0');
150+
});
138151
});
139152

140153
describe('assignImageSource', () => {

src/image-tag.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import path from 'path';
22

3-
const IMAGE_DIGEST_KEYS = ['squid', 'agent', 'agent-act', 'api-proxy', 'cli-proxy'] as const;
3+
const IMAGE_DIGEST_KEYS = ['squid', 'agent', 'agent-act', 'api-proxy', 'cli-proxy', 'build-tools'] as const;
44

55
type ImageDigestKey = typeof IMAGE_DIGEST_KEYS[number];
66

7-
interface ParsedImageTag {
7+
export interface ParsedImageTag {
88
tag: string;
99
digests: Partial<Record<ImageDigestKey, string>>;
1010
}

src/services/optional-services.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function filterAgentVolumesForSysroot(
9898
function assembleSysrootService(
9999
params: AssembleOptionalServicesParams,
100100
registry: string,
101-
imageTag: string,
101+
parsedTag: import('../image-tag').ParsedImageTag,
102102
sysrootActive: boolean,
103103
): void {
104104
if (!sysrootActive) return;
@@ -119,7 +119,7 @@ function assembleSysrootService(
119119
const sysrootService = buildSysrootStageService({
120120
config,
121121
registry,
122-
imageTag,
122+
parsedTag,
123123
});
124124
services['sysroot-stage'] = sysrootService;
125125

@@ -249,7 +249,7 @@ export function assembleOptionalServices(
249249
const sysrootActive = isSysrootEnabled(config);
250250

251251
presetSidecarIpEnvVars(environment, config, networkConfig);
252-
assembleSysrootService(params, imageConfig.registry, imageConfig.parsedTag.tag, sysrootActive);
252+
assembleSysrootService(params, imageConfig.registry, imageConfig.parsedTag, sysrootActive);
253253
assembleIptablesInitService(params, networkIsolation);
254254
assembleApiProxyService(params);
255255
assembleDohProxyService(params);

src/services/sysroot-service.test.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { buildSysrootStageService, isSysrootEnabled, resolveSysrootImage } from './sysroot-service';
2+
import { parseImageTag } from '../image-tag';
23
import { WrapperConfig } from '../types';
34

45
// Minimal WrapperConfig for testing
@@ -62,14 +63,25 @@ describe('resolveSysrootImage', () => {
6263
});
6364
expect(resolveSysrootImage(config)).toBe('ghcr.io/my-org/custom-sysroot:v1');
6465
});
66+
67+
it('includes sha256 digest in resolved image when provided in imageTag', () => {
68+
const digest = 'sha256:' + 'f'.repeat(64);
69+
const config = makeConfig({
70+
runnerTopology: 'arc-dind',
71+
imageTag: `v0.28.0,build-tools=${digest}`,
72+
});
73+
expect(resolveSysrootImage(config)).toBe(
74+
`ghcr.io/github/gh-aw-firewall/build-tools:v0.28.0@${digest}`,
75+
);
76+
});
6577
});
6678

6779
describe('buildSysrootStageService', () => {
6880
it('generates a service with correct container name', () => {
6981
const service = buildSysrootStageService({
7082
config: makeConfig({ runnerTopology: 'arc-dind' }),
7183
registry: 'ghcr.io/github/gh-aw-firewall',
72-
imageTag: 'latest',
84+
parsedTag: parseImageTag('latest'),
7385
});
7486
expect(service.container_name).toBe('awf-sysroot-stage');
7587
});
@@ -78,19 +90,29 @@ describe('buildSysrootStageService', () => {
7890
const service = buildSysrootStageService({
7991
config: makeConfig({ runnerTopology: 'arc-dind' }),
8092
registry: 'ghcr.io/github/gh-aw-firewall',
81-
imageTag: '0.28.0',
93+
parsedTag: parseImageTag('0.28.0'),
8294
});
8395
expect(service.image).toBe('ghcr.io/github/gh-aw-firewall/build-tools:0.28.0');
8496
});
8597

98+
it('appends sha256 digest when build-tools digest is provided', () => {
99+
const digest = 'sha256:' + 'e'.repeat(64);
100+
const service = buildSysrootStageService({
101+
config: makeConfig({ runnerTopology: 'arc-dind' }),
102+
registry: 'ghcr.io/github/gh-aw-firewall',
103+
parsedTag: parseImageTag(`0.28.0,build-tools=${digest}`),
104+
});
105+
expect(service.image).toBe(`ghcr.io/github/gh-aw-firewall/build-tools:0.28.0@${digest}`);
106+
});
107+
86108
it('uses explicit sysrootImage when configured', () => {
87109
const service = buildSysrootStageService({
88110
config: makeConfig({
89111
runnerTopology: 'arc-dind',
90112
sysrootImage: 'ghcr.io/my-org/sysroot:v2',
91113
}),
92114
registry: 'ghcr.io/github/gh-aw-firewall',
93-
imageTag: 'latest',
115+
parsedTag: parseImageTag('latest'),
94116
});
95117
expect(service.image).toBe('ghcr.io/my-org/sysroot:v2');
96118
});
@@ -99,7 +121,7 @@ describe('buildSysrootStageService', () => {
99121
const service = buildSysrootStageService({
100122
config: makeConfig({ runnerTopology: 'arc-dind' }),
101123
registry: 'ghcr.io/github/gh-aw-firewall',
102-
imageTag: 'latest',
124+
parsedTag: parseImageTag('latest'),
103125
});
104126
expect(service.volumes).toEqual(['sysroot:/sysroot']);
105127
});
@@ -108,7 +130,7 @@ describe('buildSysrootStageService', () => {
108130
const service = buildSysrootStageService({
109131
config: makeConfig({ runnerTopology: 'arc-dind' }),
110132
registry: 'ghcr.io/github/gh-aw-firewall',
111-
imageTag: 'latest',
133+
parsedTag: parseImageTag('latest'),
112134
});
113135
expect(service.entrypoint).toEqual(['/bin/sh', '-c']);
114136
expect(service.command).toHaveLength(1);
@@ -120,7 +142,7 @@ describe('buildSysrootStageService', () => {
120142
const service = buildSysrootStageService({
121143
config: makeConfig({ runnerTopology: 'arc-dind' }),
122144
registry: 'ghcr.io/github/gh-aw-firewall',
123-
imageTag: 'latest',
145+
parsedTag: parseImageTag('latest'),
124146
});
125147
expect(service.command[0]).toContain('.awf-sysroot-ready');
126148
});
@@ -129,7 +151,7 @@ describe('buildSysrootStageService', () => {
129151
const service = buildSysrootStageService({
130152
config: makeConfig({ runnerTopology: 'arc-dind' }),
131153
registry: 'ghcr.io/github/gh-aw-firewall',
132-
imageTag: 'latest',
154+
parsedTag: parseImageTag('latest'),
133155
});
134156
// Docker Compose treats $var as variable interpolation; $$ escapes to literal $
135157
expect(service.command[0]).toContain('/$$d');
@@ -140,7 +162,7 @@ describe('buildSysrootStageService', () => {
140162
const service = buildSysrootStageService({
141163
config: makeConfig({ runnerTopology: 'arc-dind' }),
142164
registry: 'ghcr.io/github/gh-aw-firewall',
143-
imageTag: 'latest',
165+
parsedTag: parseImageTag('latest'),
144166
});
145167
expect(service.network_mode).toBe('none');
146168
});
@@ -149,7 +171,7 @@ describe('buildSysrootStageService', () => {
149171
const service = buildSysrootStageService({
150172
config: makeConfig({ runnerTopology: 'arc-dind' }),
151173
registry: 'ghcr.io/github/gh-aw-firewall',
152-
imageTag: 'latest',
174+
parsedTag: parseImageTag('latest'),
153175
});
154176
expect(service.command[0]).toContain('if [ -d /lib64 ]; then cp -a /lib64 /sysroot/; fi;');
155177
expect(service.command[0]).not.toContain('|| true');

src/services/sysroot-service.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { WrapperConfig } from '../types';
22
import { logger } from '../logger';
3+
import { ParsedImageTag, buildRuntimeImageRef, parseImageTag } from '../image-tag';
34

45
/**
56
* Default sysroot image when runner.topology is 'arc-dind' and no explicit
67
* sysrootImage is configured.
78
*/
8-
function defaultSysrootImage(registry: string, tag: string): string {
9-
return `${registry}/build-tools:${tag}`;
9+
function defaultSysrootImage(registry: string, parsedTag: ParsedImageTag): string {
10+
return buildRuntimeImageRef(registry, 'build-tools', parsedTag);
1011
}
1112

1213
interface SysrootServiceParams {
1314
config: WrapperConfig;
1415
registry: string;
15-
imageTag: string;
16+
parsedTag: ParsedImageTag;
1617
}
1718

1819
/**
@@ -26,8 +27,8 @@ interface SysrootServiceParams {
2627
* /lib64 is conditionally copied (exists on amd64, not on arm64).
2728
*/
2829
export function buildSysrootStageService(params: SysrootServiceParams): any {
29-
const { config, registry, imageTag } = params;
30-
const image = config.sysrootImage || defaultSysrootImage(registry, imageTag);
30+
const { config, registry, parsedTag } = params;
31+
const image = config.sysrootImage || defaultSysrootImage(registry, parsedTag);
3132

3233
logger.info(`ARC/DinD: sysroot-stage will use image ${image}`);
3334

@@ -70,6 +71,7 @@ export function isSysrootEnabled(config: WrapperConfig): boolean {
7071
export function resolveSysrootImage(config: WrapperConfig): string | undefined {
7172
if (!isSysrootEnabled(config)) return undefined;
7273
const registry = config.imageRegistry || 'ghcr.io/github/gh-aw-firewall';
73-
const tag = config.imageTag || 'latest';
74-
return config.sysrootImage || defaultSysrootImage(registry, tag);
74+
if (config.sysrootImage) return config.sysrootImage;
75+
const parsedTag = parseImageTag(config.imageTag || 'latest');
76+
return buildRuntimeImageRef(registry, 'build-tools', parsedTag);
7577
}

0 commit comments

Comments
 (0)