-
Notifications
You must be signed in to change notification settings - Fork 52
fix(agent-byo): resolve Dockerfile path relative to cwd, matching add-policy wizard #1160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { resolveDockerfileSource } from '../useAddAgent'; | ||
| import { resolve } from 'path'; | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| /** | ||
| * Regression tests for issue #1128: the Dockerfile picker in the agent BYO | ||
| * wizard must resolve user-supplied paths relative to the directory the user | ||
| * invoked `agentcore` from, NOT relative to the (possibly not-yet-created) | ||
| * project's code directory. | ||
| */ | ||
| describe('resolveDockerfileSource', () => { | ||
| it('returns null for undefined (no dockerfile selected)', () => { | ||
| expect(resolveDockerfileSource(undefined)).toBeNull(); | ||
| }); | ||
|
|
||
| it('returns null for empty string (user pressed Enter for default)', () => { | ||
| expect(resolveDockerfileSource('')).toBeNull(); | ||
| }); | ||
|
|
||
| it('returns null for a bare filename (no slash)', () => { | ||
| // A bare filename means the file is expected to already live inside the | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test locks in the "bare filename → null (no copy)" behavior. Given that the reporter's exact input in #1128 is a bare filename ( |
||
| // build context's code directory; no copy is needed. | ||
| expect(resolveDockerfileSource('Dockerfile')).toBeNull(); | ||
| expect(resolveDockerfileSource('Dockerfile.dev')).toBeNull(); | ||
| }); | ||
|
|
||
| it('resolves a relative path against the supplied cwd, not against any project root', () => { | ||
| const cwd = '/home/user/workplace/.github'; | ||
| expect(resolveDockerfileSource('docker/MyDockerfile.dev', cwd)).toBe(resolve(cwd, 'docker/MyDockerfile.dev')); | ||
| }); | ||
|
|
||
| it('resolves a nested relative path against the supplied cwd', () => { | ||
| const cwd = '/home/user/workplace/.github'; | ||
| expect(resolveDockerfileSource('subdir/MyDockerfile', cwd)).toBe(resolve(cwd, 'subdir/MyDockerfile')); | ||
| }); | ||
|
|
||
| it('resolves "./Dockerfile" against the supplied cwd', () => { | ||
| const cwd = '/home/user/workplace/.github'; | ||
| // "./Dockerfile" contains "/", so it is treated as a real path, not a | ||
| // bare filename. resolve() collapses the "./" segment. | ||
| expect(resolveDockerfileSource('./Dockerfile', cwd)).toBe(resolve(cwd, 'Dockerfile')); | ||
| }); | ||
|
|
||
| it('preserves absolute paths (they are not re-rooted)', () => { | ||
| const cwd = '/home/user/workplace/.github'; | ||
| const abs = '/tmp/elsewhere/Dockerfile'; | ||
| expect(resolveDockerfileSource(abs, cwd)).toBe(abs); | ||
| }); | ||
|
|
||
| it('defaults to process.cwd() when no cwd argument is supplied', () => { | ||
| const original = process.cwd(); | ||
| expect(resolveDockerfileSource('subdir/Dockerfile')).toBe(resolve(original, 'subdir/Dockerfile')); | ||
| }); | ||
|
|
||
| it('does NOT resolve relative to a project root (regression for #1128)', () => { | ||
| // The bug: previously the picker rooted at <projectRoot>/<codeLocation>, | ||
| // e.g. /home/user/.github/myproj/app/myagent/. A user typing | ||
| // "MyDockerfile" from /home/user/.github would have it looked up at | ||
| // /home/user/.github/myproj/app/myagent/MyDockerfile — wrong. | ||
| const cwd = '/home/user/workplace/.github'; | ||
| const resolved = resolveDockerfileSource('subdir/MyDockerfile', cwd); | ||
| expect(resolved).toBe('/home/user/workplace/.github/subdir/MyDockerfile'); | ||
| expect(resolved).not.toContain('/app/'); | ||
| expect(resolved).not.toContain('/myproj/'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,30 @@ export interface AddAgentError { | |
|
|
||
| export type AddAgentOutcome = AddAgentCreateResult | AddAgentByoResult | AddAgentError; | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────────── | ||
| // Dockerfile source resolution | ||
| // ───────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /** | ||
| * Resolve the absolute source path of a user-supplied Dockerfile. | ||
| * | ||
| * The path is interpreted relative to the directory the user invoked | ||
| * `agentcore` from (process.cwd()), matching the "add policy" file picker | ||
| * (issue #1128). Absolute paths are returned unchanged. A `null` return | ||
| * value means the caller should not attempt to copy the file (the user | ||
| * either left the field empty, indicating they want the default | ||
| * Dockerfile, or supplied a bare filename indicating the file already | ||
| * lives inside the build context). | ||
| */ | ||
| export function resolveDockerfileSource(dockerfile: string | undefined, cwd: string = process.cwd()): string | null { | ||
| // Empty / undefined / bare-filename inputs do not need resolution: empty | ||
| // means "use default", bare filename means "already in code dir". | ||
| if (!dockerfile?.includes('/')) { | ||
| return null; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This short-circuits on any input without a A few ways to address this:
Related: whichever behavior you pick for this helper also needs to be applied in |
||
| return resolve(cwd, dockerfile); | ||
| } | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────────── | ||
| // Config Mappers | ||
| // ───────────────────────────────────────────────────────────────────────────── | ||
|
|
@@ -260,14 +284,16 @@ async function handleCreatePath( | |
| const renderer = createRenderer(renderConfig); | ||
| await renderer.render({ outputDir: projectRoot }); | ||
|
|
||
| // If dockerfile is a path (contains /), copy it into the agent directory (overwriting template default) | ||
| if (generateConfig.dockerfile?.includes('/')) { | ||
| const sourcePath = resolve(projectRoot, generateConfig.dockerfile); | ||
| if (!existsSync(sourcePath)) { | ||
| return { ok: false, error: `Dockerfile not found at ${sourcePath}` }; | ||
| // If dockerfile is a path (contains /), copy it into the agent directory (overwriting template default). | ||
| // The path is interpreted relative to the directory the user invoked agentcore from (process.cwd()), | ||
| // matching the behavior of the "add policy" file picker (issue #1128). | ||
| const createSourcePath = resolveDockerfileSource(generateConfig.dockerfile); | ||
| if (createSourcePath !== null) { | ||
| if (!existsSync(createSourcePath)) { | ||
| return { ok: false, error: `Dockerfile not found at ${createSourcePath}` }; | ||
| } | ||
| const filename = basename(sourcePath); | ||
| copyFileSync(sourcePath, join(agentPath, filename)); | ||
| const filename = basename(createSourcePath); | ||
| copyFileSync(createSourcePath, join(agentPath, filename)); | ||
| generateConfig.dockerfile = filename; | ||
| } | ||
|
|
||
|
|
@@ -369,15 +395,17 @@ async function handleByoPath( | |
| const codeDir = join(projectRoot, config.codeLocation.replace(/\/$/, '')); | ||
| mkdirSync(codeDir, { recursive: true }); | ||
|
|
||
| // If dockerfile is a path (contains /), copy it into the code directory and use the filename | ||
| // If dockerfile is a path (contains /), copy it into the code directory and use the filename. | ||
| // The user-supplied path is resolved relative to process.cwd() — the directory the user | ||
| // invoked agentcore from — matching the "add policy" file picker (issue #1128). | ||
| let dockerfileName = config.dockerfile; | ||
| if (dockerfileName?.includes('/')) { | ||
| const sourcePath = resolve(projectRoot, dockerfileName); | ||
| if (!existsSync(sourcePath)) { | ||
| return { ok: false, error: `Dockerfile not found at ${sourcePath}` }; | ||
| const byoSourcePath = resolveDockerfileSource(dockerfileName); | ||
| if (byoSourcePath !== null) { | ||
| if (!existsSync(byoSourcePath)) { | ||
| return { ok: false, error: `Dockerfile not found at ${byoSourcePath}` }; | ||
| } | ||
| dockerfileName = basename(sourcePath); | ||
| copyFileSync(sourcePath, join(codeDir, dockerfileName)); | ||
| dockerfileName = basename(byoSourcePath); | ||
| copyFileSync(byoSourcePath, join(codeDir, dockerfileName)); | ||
| } | ||
|
|
||
| const project = await configIO.readProjectSpec(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,7 +36,6 @@ import { | |
| } from './types'; | ||
| import type { useGenerateWizard } from './useGenerateWizard'; | ||
| import { Box, Text, useInput } from 'ink'; | ||
| import { basename } from 'path'; | ||
|
|
||
| // Helper to get provider display name and env var name from ModelProvider | ||
| function getProviderInfo(provider: ModelProvider): { name: string; envVarName: string } { | ||
|
|
@@ -234,7 +233,11 @@ export function GenerateWizardUI({ | |
| allowEmpty | ||
| emptyHelpText="Press Enter to use the default Dockerfile" | ||
| onSubmit={value => { | ||
| wizard.setDockerfile(value ? basename(value) : undefined); | ||
| // Preserve the full user-supplied path so handleCreatePath can | ||
| // locate and copy the source file. The path is collapsed to a | ||
| // basename only after a successful copy into the agent dir | ||
| // (issue #1128). | ||
| wizard.setDockerfile(value || undefined); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the regression path. Removing
So if a user runs Options to fix:
Whichever route you take, please add a test (ideally an integration-style one) that exercises |
||
| }} | ||
| onCancel={onBack} | ||
| /> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same regression as the matching change in
GenerateWizardUI.tsx: the full path is now preserved inbyoConfig.dockerfile, and in theagentcore createflow it reachesmapByoConfigToAgent(useAddAgent.ts:49) viauseCreateFlow.ts:378without being basename'd or copied. The resulting agent spec failsAgentEnvSpecSchema.dockerfilevalidation for any/-containing input.This branch is fine for the
agent addpath (wherehandleByoPathcopies and basename's), but we need thecreatepath to do the same work before this can land. See the inline comment onGenerateWizardUI.tsxfor options.