fix: mode-aware directory creation + allow absolute bundle paths#10
fix: mode-aware directory creation + allow absolute bundle paths#10danielmeppiel merged 6 commits intomainfrom
Conversation
In isolated mode with a non-existent working directory (e.g.
/tmp/gh-aw/apm-workspace), generateManifest fails with ENOENT
because the directory doesn't exist yet.
Add fs.mkdirSync(resolvedDir, { recursive: true }) in run()
right after resolving the working directory, before any file
operations.
Also adds run() test coverage for isolated mode with a
non-existent directory, and expands mock setup to support
full run() testing.
There was a problem hiding this comment.
Pull request overview
Fixes isolated-mode failures when working-directory points to a non-existent path by ensuring the directory exists before any file operations (e.g., generating apm.yml).
Changes:
- Create the resolved working directory via
fs.mkdirSync(..., { recursive: true })early inrun(). - Add a
run()integration-style test to verify non-existent nested working directories are created andapm.ymlis written. - Update the compiled
dist/index.jsto reflect the source change.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/runner.ts | Ensures the working directory exists before manifest generation / other filesystem work. |
| src/tests/runner.test.ts | Adds coverage for isolated mode with a non-existent nested working directory. |
| dist/index.js | Updates built action output to include the working-directory creation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const mockExec = jest.fn<() => Promise<number>>(); | ||
| jest.unstable_mockModule('@actions/exec', () => ({ | ||
| exec: mockExec, | ||
| })); |
There was a problem hiding this comment.
mockExec is typed as a no-arg function, but runApm calls @actions/exec.exec with (command, args, options). Consider typing the mock with the real signature so TypeScript can catch accidental misuse and so expectations like toHaveBeenCalledWith(...) are easier to write/maintain.
resolveLocalBundle rejected any bundle path outside the working directory, including absolute paths like /tmp/gh-aw/apm-bundle/*.tar.gz that the user explicitly specified. This blocks gh-aw's restore step where download-artifact puts the bundle in /tmp/. The path traversal check now only applies to relative patterns (where traversal via ../ is the actual risk). Absolute paths are user-explicit and bypass the containment check. Added test: 'allows absolute bundle paths outside workspace'.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…lt mode - isolated/pack/bundle modes: action creates working directory (owns lifecycle) - default mode: fail fast if directory doesn't exist (caller owns the project) - Clear comments explaining the contract in runner.ts - Updated action.yml and README.md input descriptions - Added test: 'fails fast when working directory does not exist in default mode'
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Move mutual-exclusion check above directory creation so invalid configs never create directories as a side effect.
Problem
Two bugs prevent gh-aw's apm-action integration from working end-to-end when using
/tmp/paths outsideGITHUB_WORKSPACE:Bug 1: ENOENT on isolated mode with non-existent working directory
In isolated mode with
working-directory: /tmp/gh-aw/apm-workspace, the directory doesn't exist on the runner.generateManifestfails with:Workflow run: https://github.com/github/gh-aw/actions/runs/22928940243
Bug 2: Path traversal guard rejects absolute bundle paths in restore mode
The restore step uses
bundle: /tmp/gh-aw/apm-bundle/*.tar.gz(downloaded byactions/download-artifact).resolveLocalBundlerejects this because the bundle is outsideGITHUB_WORKSPACE:This bug hadn't surfaced yet because the pack step (bug 1) failed first, skipping the agent job entirely.
Root Cause
Both bugs share the same root cause: the action assumed all paths are within
GITHUB_WORKSPACE. gh-aw uses/tmp/gh-aw/for isolation.Fixes (5 commits)
Commit 1: Create working directory before generating manifest
Add
fs.mkdirSync(resolvedDir, { recursive: true })inrun().Commit 2: Allow absolute bundle paths
The path traversal check in
resolveLocalBundlenow only applies to relative patterns (where../traversal is the actual risk). Absolute paths are user-explicit and bypass the containment check.Commit 3: Fix lint — use
unknowninstead ofas anyThe test mock parameter used
as anywith a misplaced eslint-disable comment. Changed parameter type tounknownfor proper Jest mock compatibility — no cast or disable comment needed.Commit 4: Make directory creation mode-aware with fail-fast
The unconditional
mkdirSyncfrom commit 1 was too broad — it silently created empty directories even in non-isolated mode, which would just fail later atapm install. Refined to a clear contract:actionOwnsDir = true): the action creates the working directory. These modes bootstrap everything from scratch.actionOwnsDir = false): fail fast with a descriptive error if the directory doesn't exist, guiding the developer to useisolated: true.Updated
action.ymlandREADME.mdinput descriptions to document this contract.Commit 5: Use "non-isolated mode" terminology
Replaced "default mode" with "non-isolated mode" in all docs, comments, and error messages for clarity.
End-to-End Trace (gh-aw smoke workflow)
Pack step (activation job):
resolvedDir = '/tmp/gh-aw/apm-workspace',actionOwnsDir = true(isolated)mkdirSynccreates it ✅clearPrimitives— early return (no.github/) ✅generateManifest— writesapm.yml✅apm install+apm pack --target claude --archive✅bundle-path: /tmp/gh-aw/apm-workspace/build/claude.tar.gz✅Restore step (agent job):
resolvedDir = GITHUB_WORKSPACE(no working-directory),actionOwnsDir = true(bundle)bundleInput = '/tmp/gh-aw/apm-bundle/*.tar.gz'resolveLocalBundle— absolute path, skips traversal check ✅extractBundle— extracts into GITHUB_WORKSPACE ✅Test Coverage (22 tests, all pass)
run › creates working directory when it does not exist (isolated mode)run › fails fast when working directory does not exist in non-isolated moderesolveLocalBundle › allows absolute bundle paths outside workspaceresolveLocalBundle › throws when resolved path is outside workspace(relative traversal still blocked)