Skip to content

Commit 98e7aeb

Browse files
authored
support changing the working dir (#47)
* support changing the working dir Signed-off-by: Jiaxiao (mossaka) Zhou <duibao55328@gmail.com> * Add container working directory support and tests Signed-off-by: Jiaxiao (mossaka) Zhou <duibao55328@gmail.com> --------- Signed-off-by: Jiaxiao (mossaka) Zhou <duibao55328@gmail.com>
1 parent 5b74684 commit 98e7aeb

File tree

6 files changed

+221
-2
lines changed

6 files changed

+221
-2
lines changed

src/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ program
254254
(value, previous: string[] = []) => [...previous, value],
255255
[]
256256
)
257+
.option(
258+
'--container-workdir <dir>',
259+
'Working directory inside the container (should match GITHUB_WORKSPACE for path consistency)'
260+
)
257261
.argument('[args...]', 'Command and arguments to execute (use -- to separate from options)')
258262
.action(async (args: string[], options) => {
259263
// Require -- separator for passing command arguments
@@ -317,6 +321,7 @@ program
317321
additionalEnv: Object.keys(additionalEnv).length > 0 ? additionalEnv : undefined,
318322
envAll: options.envAll,
319323
volumeMounts,
324+
containerWorkDir: options.containerWorkdir,
320325
};
321326

322327
// Warn if --env-all is used

src/docker-manager.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,5 +294,79 @@ describe('docker-manager', () => {
294294
}
295295
}
296296
});
297+
298+
describe('containerWorkDir option', () => {
299+
it('should not set working_dir when containerWorkDir is not specified', () => {
300+
const result = generateDockerCompose(mockConfig, mockNetworkConfig);
301+
302+
expect(result.services.copilot.working_dir).toBeUndefined();
303+
});
304+
305+
it('should set working_dir when containerWorkDir is specified', () => {
306+
const config: WrapperConfig = {
307+
...mockConfig,
308+
containerWorkDir: '/home/runner/work/repo/repo',
309+
};
310+
const result = generateDockerCompose(config, mockNetworkConfig);
311+
312+
expect(result.services.copilot.working_dir).toBe('/home/runner/work/repo/repo');
313+
});
314+
315+
it('should set working_dir to /workspace when containerWorkDir is /workspace', () => {
316+
const config: WrapperConfig = {
317+
...mockConfig,
318+
containerWorkDir: '/workspace',
319+
};
320+
const result = generateDockerCompose(config, mockNetworkConfig);
321+
322+
expect(result.services.copilot.working_dir).toBe('/workspace');
323+
});
324+
325+
it('should handle paths with special characters', () => {
326+
const config: WrapperConfig = {
327+
...mockConfig,
328+
containerWorkDir: '/home/user/my-project with spaces',
329+
};
330+
const result = generateDockerCompose(config, mockNetworkConfig);
331+
332+
expect(result.services.copilot.working_dir).toBe('/home/user/my-project with spaces');
333+
});
334+
335+
it('should preserve working_dir alongside other copilot service config', () => {
336+
const config: WrapperConfig = {
337+
...mockConfig,
338+
containerWorkDir: '/custom/workdir',
339+
envAll: true,
340+
};
341+
const result = generateDockerCompose(config, mockNetworkConfig);
342+
343+
// Verify working_dir is set
344+
expect(result.services.copilot.working_dir).toBe('/custom/workdir');
345+
// Verify other config is still present
346+
expect(result.services.copilot.container_name).toBe('awf-copilot');
347+
expect(result.services.copilot.cap_add).toContain('NET_ADMIN');
348+
});
349+
350+
it('should handle empty string containerWorkDir by not setting working_dir', () => {
351+
const config: WrapperConfig = {
352+
...mockConfig,
353+
containerWorkDir: '',
354+
};
355+
const result = generateDockerCompose(config, mockNetworkConfig);
356+
357+
// Empty string is falsy, so working_dir should not be set
358+
expect(result.services.copilot.working_dir).toBeUndefined();
359+
});
360+
361+
it('should handle absolute paths correctly', () => {
362+
const config: WrapperConfig = {
363+
...mockConfig,
364+
containerWorkDir: '/var/lib/app/data',
365+
};
366+
const result = generateDockerCompose(config, mockNetworkConfig);
367+
368+
expect(result.services.copilot.working_dir).toBe('/var/lib/app/data');
369+
});
370+
});
297371
});
298372
});

src/docker-manager.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,12 @@ export function generateDockerCompose(
257257
command: ['/bin/bash', '-c', config.copilotCommand.replace(/\$/g, '$$$$')],
258258
};
259259

260+
// Set working directory if specified (overrides Dockerfile WORKDIR)
261+
if (config.containerWorkDir) {
262+
copilotService.working_dir = config.containerWorkDir;
263+
logger.debug(`Set container working directory to: ${config.containerWorkDir}`);
264+
}
265+
260266
// Use GHCR image or build locally
261267
if (useGHCR) {
262268
copilotService.image = `${registry}/copilot:${tag}`;

src/types.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,19 @@ export interface WrapperConfig {
155155
* @example ['/workspace:/workspace:ro', '/data:/data:rw']
156156
*/
157157
volumeMounts?: string[];
158+
159+
/**
160+
* Working directory inside the copilot container
161+
*
162+
* Sets the initial working directory (pwd) for command execution.
163+
* This overrides the Dockerfile's WORKDIR and should match GITHUB_WORKSPACE
164+
* for path consistency with AI prompts.
165+
*
166+
* When not specified, defaults to the container's WORKDIR (/workspace).
167+
*
168+
* @example '/home/runner/work/repo/repo'
169+
*/
170+
containerWorkDir?: string;
158171
}
159172

160173
/**
@@ -447,14 +460,25 @@ export interface DockerService {
447460

448461
/**
449462
* Port mappings from host to container
450-
*
463+
*
451464
* Array of port mappings in format 'host:container' or 'host:container/protocol'.
452465
* The firewall typically doesn't expose ports as communication happens over
453466
* the Docker network.
454-
*
467+
*
455468
* @example ['8080:80', '443:443/tcp']
456469
*/
457470
ports?: string[];
471+
472+
/**
473+
* Working directory inside the container
474+
*
475+
* Sets the initial working directory (pwd) for command execution.
476+
* This overrides the WORKDIR specified in the Dockerfile.
477+
*
478+
* @example '/home/runner/work/repo/repo'
479+
* @example '/workspace'
480+
*/
481+
working_dir?: string;
458482
}
459483

460484
/**

tests/fixtures/awf-runner.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface AwfOptions {
1313
timeout?: number; // milliseconds
1414
env?: Record<string, string>;
1515
volumeMounts?: string[]; // Volume mounts in format: host_path:container_path[:mode]
16+
containerWorkDir?: string; // Working directory inside the container
1617
}
1718

1819
export interface AwfResult {
@@ -74,6 +75,11 @@ export class AwfRunner {
7475
});
7576
}
7677

78+
// Add container working directory
79+
if (options.containerWorkDir) {
80+
args.push('--container-workdir', options.containerWorkDir);
81+
}
82+
7783
// Add -- separator before command
7884
args.push('--');
7985

@@ -166,6 +172,11 @@ export class AwfRunner {
166172
});
167173
}
168174

175+
// Add container working directory
176+
if (options.containerWorkDir) {
177+
args.push('--container-workdir', options.containerWorkDir);
178+
}
179+
169180
// Add -- separator before command
170181
args.push('--');
171182

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Container Working Directory Tests
3+
*
4+
* These tests verify the --container-workdir CLI option:
5+
* - Default working directory is /workspace
6+
* - Custom working directory can be set via CLI
7+
* - Commands execute from the specified working directory
8+
*/
9+
10+
/// <reference path="../jest-custom-matchers.d.ts" />
11+
12+
import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
13+
import { createRunner, AwfRunner } from '../fixtures/awf-runner';
14+
import { cleanup } from '../fixtures/cleanup';
15+
16+
describe('Container Working Directory', () => {
17+
let runner: AwfRunner;
18+
19+
beforeAll(async () => {
20+
// Run cleanup before tests to ensure clean state
21+
await cleanup(false);
22+
runner = createRunner();
23+
});
24+
25+
afterAll(async () => {
26+
// Clean up after all tests
27+
await cleanup(false);
28+
});
29+
30+
test('should use default working directory /workspace when --container-workdir not specified', async () => {
31+
const result = await runner.runWithSudo('pwd', {
32+
allowDomains: ['github.com'],
33+
logLevel: 'debug',
34+
timeout: 60000,
35+
});
36+
37+
expect(result).toSucceed();
38+
// Default WORKDIR in Dockerfile is /workspace
39+
expect(result.stdout.trim()).toContain('/workspace');
40+
}, 120000);
41+
42+
test('should use custom working directory when --container-workdir is specified', async () => {
43+
const result = await runner.runWithSudo('pwd', {
44+
allowDomains: ['github.com'],
45+
logLevel: 'debug',
46+
timeout: 60000,
47+
containerWorkDir: '/tmp',
48+
});
49+
50+
expect(result).toSucceed();
51+
expect(result.stdout.trim()).toContain('/tmp');
52+
}, 120000);
53+
54+
test('should execute commands in the specified working directory', async () => {
55+
// Create a file in /tmp and verify we can list it from /tmp working directory
56+
const result = await runner.runWithSudo(
57+
'bash -c "touch testfile.txt && ls -la | grep testfile"',
58+
{
59+
allowDomains: ['github.com'],
60+
logLevel: 'debug',
61+
timeout: 60000,
62+
containerWorkDir: '/tmp',
63+
}
64+
);
65+
66+
expect(result).toSucceed();
67+
expect(result.stdout).toContain('testfile.txt');
68+
}, 120000);
69+
70+
test('should work with home directory as working directory', async () => {
71+
const result = await runner.runWithSudo('pwd', {
72+
allowDomains: ['github.com'],
73+
logLevel: 'debug',
74+
timeout: 60000,
75+
containerWorkDir: process.env.HOME || '/root',
76+
});
77+
78+
expect(result).toSucceed();
79+
// The output should contain the home directory
80+
expect(result.stdout.trim()).toContain(process.env.HOME || '/root');
81+
}, 120000);
82+
83+
test('should allow relative path access from custom working directory', async () => {
84+
// Verify that relative paths work correctly from the custom workdir
85+
const result = await runner.runWithSudo(
86+
'bash -c "cd .. && pwd"',
87+
{
88+
allowDomains: ['github.com'],
89+
logLevel: 'debug',
90+
timeout: 60000,
91+
containerWorkDir: '/tmp',
92+
}
93+
);
94+
95+
expect(result).toSucceed();
96+
// Going up from /tmp should give us /
97+
expect(result.stdout.trim()).toContain('/');
98+
}, 120000);
99+
});

0 commit comments

Comments
 (0)