From c5c91bb40c53d6e24f333f24811b14f23a23d149 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:53:48 +0000 Subject: [PATCH 1/2] Initial plan From e74523021df30dba43e0a87e4fca106cc71558fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:01:13 +0000 Subject: [PATCH 2/2] feat: add build-tools to SHA digest pinning infrastructure - Add 'build-tools' to IMAGE_DIGEST_KEYS in src/image-tag.ts so that build-tools=sha256: is accepted as a valid key in --image-tag - Export ParsedImageTag interface from src/image-tag.ts - Refactor buildSysrootStageService() in src/services/sysroot-service.ts to accept ParsedImageTag instead of a raw imageTag string, and use buildRuntimeImageRef() so the @sha256:... suffix is appended when a digest is present - Update resolveSysrootImage() to parse imageTag via parseImageTag() and use buildRuntimeImageRef() so diagnostics also reflect the digest - Update assembleSysrootService() in optional-services.ts to pass the full parsedTag object instead of parsedTag.tag - Update tests in image-tag.test.ts and sysroot-service.test.ts to cover the new key and digest-pinning behaviour Closes #5985 --- src/image-tag.test.ts | 15 ++++++++++- src/image-tag.ts | 4 +-- src/services/optional-services.ts | 6 ++--- src/services/sysroot-service.test.ts | 40 +++++++++++++++++++++------- src/services/sysroot-service.ts | 16 ++++++----- 5 files changed, 59 insertions(+), 22 deletions(-) diff --git a/src/image-tag.test.ts b/src/image-tag.test.ts index 543dbaf61..6f8679714 100644 --- a/src/image-tag.test.ts +++ b/src/image-tag.test.ts @@ -1,7 +1,7 @@ import path from 'path'; import { parseImageTag, buildRuntimeImageRef, assignImageSource } from './image-tag'; -const IMAGE_DIGEST_KEYS = ['squid', 'agent', 'agent-act', 'api-proxy', 'cli-proxy'] as const; +const IMAGE_DIGEST_KEYS = ['squid', 'agent', 'agent-act', 'api-proxy', 'cli-proxy', 'build-tools'] as const; const VALID_DIGEST = 'sha256:' + 'a'.repeat(64); @@ -135,6 +135,19 @@ describe('buildRuntimeImageRef', () => { const ref = buildRuntimeImageRef('ghcr.io/github/gh-aw-firewall', 'agent', parsed); expect(ref).toBe('ghcr.io/github/gh-aw-firewall/agent:0.25.18'); }); + + it('should build ref for build-tools image with digest', () => { + const buildToolsDigest = 'sha256:' + 'd'.repeat(64); + const parsed = parseImageTag(`0.28.0,build-tools=${buildToolsDigest}`); + const ref = buildRuntimeImageRef('ghcr.io/github/gh-aw-firewall', 'build-tools', parsed); + expect(ref).toBe(`ghcr.io/github/gh-aw-firewall/build-tools:0.28.0@${buildToolsDigest}`); + }); + + it('should build ref for build-tools image without digest', () => { + const parsed = parseImageTag('0.28.0'); + const ref = buildRuntimeImageRef('ghcr.io/github/gh-aw-firewall', 'build-tools', parsed); + expect(ref).toBe('ghcr.io/github/gh-aw-firewall/build-tools:0.28.0'); + }); }); describe('assignImageSource', () => { diff --git a/src/image-tag.ts b/src/image-tag.ts index 069569ff5..fba120272 100644 --- a/src/image-tag.ts +++ b/src/image-tag.ts @@ -1,10 +1,10 @@ import path from 'path'; -const IMAGE_DIGEST_KEYS = ['squid', 'agent', 'agent-act', 'api-proxy', 'cli-proxy'] as const; +const IMAGE_DIGEST_KEYS = ['squid', 'agent', 'agent-act', 'api-proxy', 'cli-proxy', 'build-tools'] as const; type ImageDigestKey = typeof IMAGE_DIGEST_KEYS[number]; -interface ParsedImageTag { +export interface ParsedImageTag { tag: string; digests: Partial>; } diff --git a/src/services/optional-services.ts b/src/services/optional-services.ts index e5b9e9cb4..cbd49eb83 100644 --- a/src/services/optional-services.ts +++ b/src/services/optional-services.ts @@ -98,7 +98,7 @@ function filterAgentVolumesForSysroot( function assembleSysrootService( params: AssembleOptionalServicesParams, registry: string, - imageTag: string, + parsedTag: import('../image-tag').ParsedImageTag, sysrootActive: boolean, ): void { if (!sysrootActive) return; @@ -119,7 +119,7 @@ function assembleSysrootService( const sysrootService = buildSysrootStageService({ config, registry, - imageTag, + parsedTag, }); services['sysroot-stage'] = sysrootService; @@ -249,7 +249,7 @@ export function assembleOptionalServices( const sysrootActive = isSysrootEnabled(config); presetSidecarIpEnvVars(environment, config, networkConfig); - assembleSysrootService(params, imageConfig.registry, imageConfig.parsedTag.tag, sysrootActive); + assembleSysrootService(params, imageConfig.registry, imageConfig.parsedTag, sysrootActive); assembleIptablesInitService(params, networkIsolation); assembleApiProxyService(params); assembleDohProxyService(params); diff --git a/src/services/sysroot-service.test.ts b/src/services/sysroot-service.test.ts index 9bf25a21a..affbba519 100644 --- a/src/services/sysroot-service.test.ts +++ b/src/services/sysroot-service.test.ts @@ -1,4 +1,5 @@ import { buildSysrootStageService, isSysrootEnabled, resolveSysrootImage } from './sysroot-service'; +import { parseImageTag } from '../image-tag'; import { WrapperConfig } from '../types'; // Minimal WrapperConfig for testing @@ -62,6 +63,17 @@ describe('resolveSysrootImage', () => { }); expect(resolveSysrootImage(config)).toBe('ghcr.io/my-org/custom-sysroot:v1'); }); + + it('includes sha256 digest in resolved image when provided in imageTag', () => { + const digest = 'sha256:' + 'f'.repeat(64); + const config = makeConfig({ + runnerTopology: 'arc-dind', + imageTag: `v0.28.0,build-tools=${digest}`, + }); + expect(resolveSysrootImage(config)).toBe( + `ghcr.io/github/gh-aw-firewall/build-tools:v0.28.0@${digest}`, + ); + }); }); describe('buildSysrootStageService', () => { @@ -69,7 +81,7 @@ describe('buildSysrootStageService', () => { const service = buildSysrootStageService({ config: makeConfig({ runnerTopology: 'arc-dind' }), registry: 'ghcr.io/github/gh-aw-firewall', - imageTag: 'latest', + parsedTag: parseImageTag('latest'), }); expect(service.container_name).toBe('awf-sysroot-stage'); }); @@ -78,11 +90,21 @@ describe('buildSysrootStageService', () => { const service = buildSysrootStageService({ config: makeConfig({ runnerTopology: 'arc-dind' }), registry: 'ghcr.io/github/gh-aw-firewall', - imageTag: '0.28.0', + parsedTag: parseImageTag('0.28.0'), }); expect(service.image).toBe('ghcr.io/github/gh-aw-firewall/build-tools:0.28.0'); }); + it('appends sha256 digest when build-tools digest is provided', () => { + const digest = 'sha256:' + 'e'.repeat(64); + const service = buildSysrootStageService({ + config: makeConfig({ runnerTopology: 'arc-dind' }), + registry: 'ghcr.io/github/gh-aw-firewall', + parsedTag: parseImageTag(`0.28.0,build-tools=${digest}`), + }); + expect(service.image).toBe(`ghcr.io/github/gh-aw-firewall/build-tools:0.28.0@${digest}`); + }); + it('uses explicit sysrootImage when configured', () => { const service = buildSysrootStageService({ config: makeConfig({ @@ -90,7 +112,7 @@ describe('buildSysrootStageService', () => { sysrootImage: 'ghcr.io/my-org/sysroot:v2', }), registry: 'ghcr.io/github/gh-aw-firewall', - imageTag: 'latest', + parsedTag: parseImageTag('latest'), }); expect(service.image).toBe('ghcr.io/my-org/sysroot:v2'); }); @@ -99,7 +121,7 @@ describe('buildSysrootStageService', () => { const service = buildSysrootStageService({ config: makeConfig({ runnerTopology: 'arc-dind' }), registry: 'ghcr.io/github/gh-aw-firewall', - imageTag: 'latest', + parsedTag: parseImageTag('latest'), }); expect(service.volumes).toEqual(['sysroot:/sysroot']); }); @@ -108,7 +130,7 @@ describe('buildSysrootStageService', () => { const service = buildSysrootStageService({ config: makeConfig({ runnerTopology: 'arc-dind' }), registry: 'ghcr.io/github/gh-aw-firewall', - imageTag: 'latest', + parsedTag: parseImageTag('latest'), }); expect(service.entrypoint).toEqual(['/bin/sh', '-c']); expect(service.command).toHaveLength(1); @@ -120,7 +142,7 @@ describe('buildSysrootStageService', () => { const service = buildSysrootStageService({ config: makeConfig({ runnerTopology: 'arc-dind' }), registry: 'ghcr.io/github/gh-aw-firewall', - imageTag: 'latest', + parsedTag: parseImageTag('latest'), }); expect(service.command[0]).toContain('.awf-sysroot-ready'); }); @@ -129,7 +151,7 @@ describe('buildSysrootStageService', () => { const service = buildSysrootStageService({ config: makeConfig({ runnerTopology: 'arc-dind' }), registry: 'ghcr.io/github/gh-aw-firewall', - imageTag: 'latest', + parsedTag: parseImageTag('latest'), }); // Docker Compose treats $var as variable interpolation; $$ escapes to literal $ expect(service.command[0]).toContain('/$$d'); @@ -140,7 +162,7 @@ describe('buildSysrootStageService', () => { const service = buildSysrootStageService({ config: makeConfig({ runnerTopology: 'arc-dind' }), registry: 'ghcr.io/github/gh-aw-firewall', - imageTag: 'latest', + parsedTag: parseImageTag('latest'), }); expect(service.network_mode).toBe('none'); }); @@ -149,7 +171,7 @@ describe('buildSysrootStageService', () => { const service = buildSysrootStageService({ config: makeConfig({ runnerTopology: 'arc-dind' }), registry: 'ghcr.io/github/gh-aw-firewall', - imageTag: 'latest', + parsedTag: parseImageTag('latest'), }); expect(service.command[0]).toContain('if [ -d /lib64 ]; then cp -a /lib64 /sysroot/; fi;'); expect(service.command[0]).not.toContain('|| true'); diff --git a/src/services/sysroot-service.ts b/src/services/sysroot-service.ts index 45e2f9b3a..d47051944 100644 --- a/src/services/sysroot-service.ts +++ b/src/services/sysroot-service.ts @@ -1,18 +1,19 @@ import { WrapperConfig } from '../types'; import { logger } from '../logger'; +import { ParsedImageTag, buildRuntimeImageRef, parseImageTag } from '../image-tag'; /** * Default sysroot image when runner.topology is 'arc-dind' and no explicit * sysrootImage is configured. */ -function defaultSysrootImage(registry: string, tag: string): string { - return `${registry}/build-tools:${tag}`; +function defaultSysrootImage(registry: string, parsedTag: ParsedImageTag): string { + return buildRuntimeImageRef(registry, 'build-tools', parsedTag); } interface SysrootServiceParams { config: WrapperConfig; registry: string; - imageTag: string; + parsedTag: ParsedImageTag; } /** @@ -26,8 +27,8 @@ interface SysrootServiceParams { * /lib64 is conditionally copied (exists on amd64, not on arm64). */ export function buildSysrootStageService(params: SysrootServiceParams): any { - const { config, registry, imageTag } = params; - const image = config.sysrootImage || defaultSysrootImage(registry, imageTag); + const { config, registry, parsedTag } = params; + const image = config.sysrootImage || defaultSysrootImage(registry, parsedTag); logger.info(`ARC/DinD: sysroot-stage will use image ${image}`); @@ -70,6 +71,7 @@ export function isSysrootEnabled(config: WrapperConfig): boolean { export function resolveSysrootImage(config: WrapperConfig): string | undefined { if (!isSysrootEnabled(config)) return undefined; const registry = config.imageRegistry || 'ghcr.io/github/gh-aw-firewall'; - const tag = config.imageTag || 'latest'; - return config.sysrootImage || defaultSysrootImage(registry, tag); + if (config.sysrootImage) return config.sysrootImage; + const parsedTag = parseImageTag(config.imageTag || 'latest'); + return buildRuntimeImageRef(registry, 'build-tools', parsedTag); }