Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/image-tag.test.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -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', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/image-tag.ts
Original file line number Diff line number Diff line change
@@ -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<Record<ImageDigestKey, string>>;
}
Expand Down
6 changes: 3 additions & 3 deletions src/services/optional-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -119,7 +119,7 @@ function assembleSysrootService(
const sysrootService = buildSysrootStageService({
config,
registry,
imageTag,
parsedTag,
});
services['sysroot-stage'] = sysrootService;

Expand Down Expand Up @@ -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);
Expand Down
40 changes: 31 additions & 9 deletions src/services/sysroot-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buildSysrootStageService, isSysrootEnabled, resolveSysrootImage } from './sysroot-service';
import { parseImageTag } from '../image-tag';
import { WrapperConfig } from '../types';

// Minimal WrapperConfig for testing
Expand Down Expand Up @@ -62,14 +63,25 @@ 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', () => {
it('generates a service with correct container name', () => {
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');
});
Expand All @@ -78,19 +90,29 @@ 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({
runnerTopology: 'arc-dind',
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');
});
Expand All @@ -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']);
});
Expand All @@ -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);
Expand All @@ -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');
});
Expand All @@ -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');
Expand All @@ -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');
});
Expand All @@ -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');
Expand Down
16 changes: 9 additions & 7 deletions src/services/sysroot-service.ts
Original file line number Diff line number Diff line change
@@ -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;
}

/**
Expand All @@ -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}`);

Expand Down Expand Up @@ -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);
}
Loading