feat: auto-copy input files to staging directory in upload_artifact handler#25832
feat: auto-copy input files to staging directory in upload_artifact handler#25832
Conversation
…andler When the agent provides a path to a file or directory that is not already in the staging directory (/tmp/gh-aw/safeoutputs/upload-artifacts/), the handler now automatically copies it from its original location before proceeding with the upload. Supports absolute paths, GITHUB_WORKSPACE- relative paths, and cwd-relative paths. Symlinks are still rejected. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/4682b046-7ef3-4d00-a4de-18dfab17cfd7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR enhances the upload_artifact safe-output handler to automatically copy requested files/directories into the staging directory when they’re referenced from outside of it (supporting absolute paths and workspace/cwd-relative paths), then proceeds with the upload using the staged copy.
Changes:
- Added auto-copy logic to
upload_artifacthandler (single-file + recursive directory copy), including symlink handling. - Updated
resolveFiles()to trigger auto-copy when a requested path isn’t already present in staging (and for absolute paths). - Updated tool descriptions and added tests covering the new auto-copy behavior.
Show a summary per file
| File | Description |
|---|---|
actions/setup/js/upload_artifact.cjs |
Implements auto-copy-to-staging behavior and integrates it into file resolution. |
actions/setup/js/upload_artifact.test.cjs |
Adds tests for absolute/workspace auto-copy, symlink rejection, and staging precedence. |
actions/setup/js/safe_outputs_tools.json |
Updates upload_artifact tool docs to reflect auto-copy and absolute/workspace paths. |
pkg/workflow/js/safe_outputs_tools.json |
Mirrors the upload_artifact tool documentation updates. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (2)
actions/setup/js/upload_artifact.cjs:205
- Auto-copying arbitrary absolute paths allows the caller to exfiltrate any readable file on the runner (e.g. /etc/*, service credentials) even when the workflow has no allowed-paths policy configured. Consider restricting auto-copy sources to trusted roots (e.g. only within GITHUB_WORKSPACE and/or process.cwd()), rejecting absolute paths outside those roots, and preserving a workspace-relative destination path (instead of basename) so allowed-paths patterns remain meaningful and collisions are avoided.
function autoCopyToStaging(reqPath) {
if (path.isAbsolute(reqPath)) {
if (!fs.existsSync(reqPath)) {
return { copied: false, relPath: "", error: `absolute path does not exist: ${reqPath}` };
}
const stat = fs.lstatSync(reqPath);
if (stat.isSymbolicLink()) {
return { copied: false, relPath: "", error: `symlinks are not allowed: ${reqPath}` };
}
// Derive a relative destination path from the basename (or relative to filesystem root for nested paths).
const relPath = path.basename(reqPath);
if (stat.isDirectory()) {
const result = copyDirectoryToStaging(reqPath, relPath);
if (result.error) return { copied: false, relPath: "", error: result.error };
core.info(`Auto-copied directory ${reqPath} to staging (${result.copiedCount} file(s))`);
} else {
const result = copySingleFileToStaging(reqPath, relPath);
if (result.error) return { copied: false, relPath: "", error: result.error };
core.info(`Auto-copied file ${reqPath} to staging as ${relPath}`);
}
return { copied: true, relPath, error: null };
}
actions/setup/js/upload_artifact.cjs:195
- The comment about deriving the destination path says "or relative to filesystem root for nested paths", but the implementation always uses path.basename(reqPath). Either adjust the comment or update the logic to match the described behavior.
return { copied: false, relPath: "", error: `symlinks are not allowed: ${reqPath}` };
}
// Derive a relative destination path from the basename (or relative to filesystem root for nested paths).
const relPath = path.basename(reqPath);
if (stat.isDirectory()) {
- Files reviewed: 4/4 changed files
- Comments generated: 1
| function copySingleFileToStaging(sourcePath, destRelPath) { | ||
| const destPath = path.join(STAGING_DIR, destRelPath); | ||
| const stat = fs.lstatSync(sourcePath); | ||
| if (stat.isSymbolicLink()) { | ||
| return { error: `symlinks are not allowed: ${sourcePath}` }; | ||
| } | ||
| if (!stat.isFile()) { | ||
| return { error: `not a regular file: ${sourcePath}` }; | ||
| } | ||
| fs.mkdirSync(path.dirname(destPath), { recursive: true }); | ||
| fs.copyFileSync(sourcePath, destPath); | ||
| return { error: null }; |
There was a problem hiding this comment.
copySingleFileToStaging() unconditionally overwrites an existing destination file in the staging directory. This can clobber pre-staged files (and makes basename-based absolute-path copies particularly risky due to collisions). Consider failing or skipping when destPath already exists, or generating a deterministic non-colliding destination path.
This issue also appears in the following locations of the same file:
- line 184
- line 191
🧪 Test Quality Sentinel ReportTest Quality Score: 87/100✅ Excellent test quality
Test Classification DetailsAll 7 test cases
HighlightsStrengths:
Minor observation (not flagged):
Language SupportTests analyzed:
Verdict
📖 Understanding Test ClassificationsDesign Tests (High Value) verify what the system does:
Implementation Tests (Low Value) verify how the system does it:
Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.
|
There was a problem hiding this comment.
✅ Test Quality Sentinel: 87/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All 7 new/modified tests verify observable behavioral contracts using real filesystem I/O, including error paths and a security invariant (symlink rejection).
|
@copilot review all comments |
Add an existence check to skip copying when the destination file already exists in the staging directory. This prevents accidental overwrites of pre-staged files, particularly with basename-based absolute-path copies where collisions are more likely. Adds a dedicated test for this case. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b2907f12-f664-4fcb-ab8e-3693b58dd893 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Addressed the reviewer's comment about |
Add documentation for two undocumented features from recent merges: 1. engine.bare (from PR #25783 v0.68.1 sync): - Documents the bare mode field that disables automatic context loading - Copilot: passes --no-custom-instructions - Claude: passes --bare - Not supported by Codex/Gemini (compiler warning emitted) - Updated engines.md feature comparison table, added Bare Mode section - Updated frontmatter-full.md with field description 2. upload-artifact safe output (from PR #25832): - Documents the upload-artifact safe output type that was missing from docs - Includes new auto-copy behavior: files referenced by original path are automatically copied to staging before upload - Added to safe-outputs.md listing and section - Added to frontmatter-full.md with full config reference Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
When the agent provides a path to a file or directory that is not already in the upload-artifact staging directory (
/tmp/gh-aw/safeoutputs/upload-artifacts/), theupload_artifacthandler now automatically copies it from its original location before proceeding with the upload.Previously, the agent was required to manually copy files into the staging directory before calling the
upload_artifacttool. Files referenced by absolute paths were rejected, and files not found in the staging directory would fail with an error. This enhancement removes that friction.Changes
actions/setup/js/upload_artifact.cjscopySingleFileToStaging()— copies a single file to the staging directory with symlink rejectioncopyDirectoryToStaging()— recursively copies a directory, skipping symlinks with warningsautoCopyToStaging()— orchestrates auto-copy for both absolute and relative paths, searchingGITHUB_WORKSPACEandcwdas fallback rootsresolveFiles()to invoke auto-copy when:actions/setup/js/safe_outputs_tools.jsonandpkg/workflow/js/safe_outputs_tools.jsonupload_artifacttool description to mention auto-copy supportpathfield description to note that absolute paths and workspace-relative paths are now acceptedactions/setup/js/upload_artifact.test.cjsGITHUB_WORKSPACESecurity