Skip to content

Commit 8d142cd

Browse files
CopilotlpcoxCopilot
authored
refactor(agent-service): extract resolveAgentImageConfig from buildAgentService (#5129)
* Initial plan * refactor(agent-service): extract resolveAgentImageConfig helper Extract image-selection logic from buildAgentService into a standalone resolveAgentImageConfig function (Sub-concern C, ~46 lines). - Isolates the three-way branch (GHCR preset / local build / passthrough) making image-provenance logic straightforward to audit - Reduces buildAgentService from 167 to ~120 lines - Exports resolveAgentImageConfig and testHelpers shim for direct unit testing without constructing a full service object - Adds 6 focused unit tests covering all three branches Closes #5118 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(agent-service): repair syntax and import ordering --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Landon Cox <landon.cox@microsoft.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent e93746e commit 8d142cd

2 files changed

Lines changed: 111 additions & 18 deletions

File tree

src/services/agent-service.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { generateDockerCompose, WrapperConfig, baseConfig, mockNetworkConfig, useTempWorkDir } from './service-test-setup.test-utils';
2+
import { testHelpers } from './agent-service';
3+
import { parseImageTag } from '../image-tag';
24
import * as fs from 'fs';
35
import * as path from 'path';
46
import * as os from 'os';
@@ -496,3 +498,71 @@ describe('agent service', () => {
496498
});
497499
});
498500
});
501+
502+
const nodePath = path;
503+
const { resolveAgentImageConfig } = testHelpers;
504+
505+
describe('resolveAgentImageConfig', () => {
506+
const projectRoot = '/fake/project';
507+
const registry = 'ghcr.io/github/gh-aw-firewall';
508+
const parsedTag = parseImageTag('latest');
509+
510+
const baseImageConfig = { useGHCR: true, registry, parsedTag, projectRoot };
511+
512+
it('returns GHCR agent image for default preset', () => {
513+
const result = resolveAgentImageConfig(
514+
{ agentImage: 'default', buildLocal: false } as any,
515+
baseImageConfig,
516+
);
517+
expect(result).toEqual({ image: 'ghcr.io/github/gh-aw-firewall/agent:latest' });
518+
});
519+
520+
it('returns GHCR agent-act image for act preset', () => {
521+
const result = resolveAgentImageConfig(
522+
{ agentImage: 'act', buildLocal: false } as any,
523+
baseImageConfig,
524+
);
525+
expect(result).toEqual({ image: 'ghcr.io/github/gh-aw-firewall/agent-act:latest' });
526+
});
527+
528+
it('returns build config for default preset with --build-local', () => {
529+
const result = resolveAgentImageConfig(
530+
{ agentImage: 'default', buildLocal: true } as any,
531+
{ ...baseImageConfig, useGHCR: false },
532+
) as any;
533+
expect(result.build).toBeDefined();
534+
expect(result.build.dockerfile).toBe('Dockerfile');
535+
expect(result.build.context).toBe(nodePath.join(projectRoot, 'containers/agent'));
536+
expect(result.build.args.BASE_IMAGE).toBeUndefined();
537+
expect(result.image).toBeUndefined();
538+
});
539+
540+
it('returns build config for act preset with --build-local', () => {
541+
const result = resolveAgentImageConfig(
542+
{ agentImage: 'act', buildLocal: true } as any,
543+
{ ...baseImageConfig, useGHCR: false },
544+
) as any;
545+
expect(result.build).toBeDefined();
546+
expect(result.build.args.BASE_IMAGE).toMatch(/catthehacker/);
547+
});
548+
549+
it('returns build config with BASE_IMAGE for custom (non-preset) image', () => {
550+
const result = resolveAgentImageConfig(
551+
{ agentImage: 'ubuntu:24.04', buildLocal: false } as any,
552+
{ ...baseImageConfig, useGHCR: false },
553+
) as any;
554+
expect(result.build).toBeDefined();
555+
expect(result.build.args.BASE_IMAGE).toBe('ubuntu:24.04');
556+
});
557+
558+
it('returns direct image passthrough when useGHCR is false, buildLocal is false, and preset image is specified', () => {
559+
// Else branch fires when: !useGHCR && !buildLocal && isPreset
560+
// (e.g. user disabled GHCR pull but did not pass --build-local, using the 'default' preset)
561+
const result = resolveAgentImageConfig(
562+
{ agentImage: 'default', buildLocal: false } as any,
563+
{ ...baseImageConfig, useGHCR: false },
564+
) as any;
565+
expect(result.image).toBe('default');
566+
expect(result.build).toBeUndefined();
567+
});
568+
});

src/services/agent-service.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ interface AgentServiceParams {
3131
*/
3232
export function buildAgentService(params: AgentServiceParams): any {
3333
const { config, networkConfig, environment, agentVolumes, dnsServers, imageConfig } = params;
34-
const { useGHCR, registry, parsedTag, projectRoot } = imageConfig;
3534

3635
// Agent service configuration
3736
const agentService: any = {
@@ -147,19 +146,39 @@ export function buildAgentService(params: AgentServiceParams): any {
147146
environment.AWF_ENABLE_HOST_ACCESS = '1';
148147
}
149148

150-
// Use GHCR image or build locally
151-
// Priority: GHCR preset images > local build (when requested) > custom images
152-
// For presets ('default', 'act'), use GHCR images
149+
Object.assign(agentService, resolveAgentImageConfig(config, imageConfig));
150+
151+
return agentService;
152+
}
153+
154+
// ─── Image Selection ─────────────────────────────────────────────────────────
155+
156+
/**
157+
* Resolves the image or build configuration for the agent container.
158+
*
159+
* Priority: GHCR preset images > local build (when requested or non-preset) > custom image passthrough
160+
*
161+
* Returns either `{ image: string }` (pull from registry) or
162+
* `{ build: { context, dockerfile, args } }` (local build), suitable for
163+
* spreading onto a Docker Compose service object.
164+
*/
165+
export function resolveAgentImageConfig(
166+
config: WrapperConfig,
167+
imageConfig: ImageBuildConfig,
168+
): { image: string } | { build: { context: string; dockerfile: string; args?: Record<string, string> } } {
169+
const { useGHCR, registry, parsedTag, projectRoot } = imageConfig;
153170
const agentImage = config.agentImage || 'default';
154171
const isPreset = agentImage === 'default' || agentImage === 'act';
155172

156-
if (useGHCR && isPreset) {
157-
// Use pre-built GHCR image for preset images
173+
if (useGHCR && isPreset && !config.buildLocal) {
158174
// The GHCR images already have the necessary setup for chroot mode
159175
const imageName = agentImage === 'act' ? 'agent-act' : 'agent';
160-
agentService.image = buildRuntimeImageRef(registry, imageName, parsedTag);
161-
logger.debug(`Using GHCR image ${agentService.image}`);
162-
} else if (config.buildLocal || !isPreset) {
176+
const image = buildRuntimeImageRef(registry, imageName, parsedTag);
177+
logger.debug(`Using GHCR image ${image}`);
178+
return { image };
179+
}
180+
181+
if (config.buildLocal || !isPreset) {
163182
// Build locally when:
164183
// 1. --build-local is explicitly specified, OR
165184
// 2. A custom (non-preset) image is specified
@@ -184,20 +203,24 @@ export function buildAgentService(params: AgentServiceParams): any {
184203
}
185204
// For 'default' preset with --build-local, use the Dockerfile's default (ubuntu:22.04)
186205

187-
agentService.build = {
188-
context: path.join(projectRoot, 'containers/agent'),
189-
dockerfile,
190-
args: buildArgs,
206+
return {
207+
build: {
208+
context: path.join(projectRoot, 'containers/agent'),
209+
dockerfile,
210+
args: buildArgs,
211+
},
191212
};
192-
} else {
193-
// Custom image specified without --build-local
194-
// Use the image directly (user is responsible for ensuring compatibility)
195-
agentService.image = agentImage;
196213
}
197214

198-
return agentService;
215+
// Custom image specified without --build-local
216+
// Use the image directly (user is responsible for ensuring compatibility)
217+
return { image: agentImage };
199218
}
200219

220+
// ts-prune-ignore-next
221+
/** @internal Exported for unit testing only */
222+
export const testHelpers = { resolveAgentImageConfig };
223+
201224
// ─── iptables-init Service ────────────────────────────────────────────────────
202225

203226
interface IptablesInitServiceParams {

0 commit comments

Comments
 (0)