Skip to content

Commit 5fb70db

Browse files
committed
fix(upload_assets): search all candidate staging dirs to resolve path-prefix mismatch (#39885)
The upload_assets (Push assets) job was failing with ERR_SYSTEM: Asset file not found: .../safeoutputs/assets/<file>.png even though the agent job succeeded, because assets were staged under a different base prefix than the job was reading from. Prior fixes (#39900, #40062) aligned individual paths, but the consumer still derived a single assetsDir — so any remaining producer/consumer prefix disagreement (e.g. RUNNER_TEMP=/home/runner/work/_temp vs the artifact download path /tmp/gh-aw) still hard-failed the whole job. Fix: build a de-duplicated list of candidate staging directories 1. parent of GH_AW_AGENT_OUTPUT (where the artifact was downloaded) 2. RUNNER_TEMP/gh-aw (where the MCP handler staged the file at runtime) 3. /tmp/gh-aw (canonical fallback) The first candidate that contains the file wins. The existing fail-soft behaviour (warn per missing file, only fail when all are missing) is preserved. The missing-file warning now lists every directory searched. Adds a regression test for the cross-prefix case. Closes #39885
1 parent 494f04d commit 5fb70db

2 files changed

Lines changed: 65 additions & 10 deletions

File tree

actions/setup/js/upload_assets.cjs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,28 @@ async function main() {
8686

8787
core.info(`Found ${uploadItems.length} upload-asset item(s)`);
8888

89-
// Derive the base directory from GH_AW_AGENT_OUTPUT when available.
90-
// In the upload_assets job, the agent artifact (including safeoutputs/assets/)
91-
// is downloaded to the same parent directory as agent_output.json, which may
92-
// differ from RUNNER_TEMP when the download path is explicitly set to /tmp/gh-aw/.
89+
// Resolve the candidate asset staging directories.
90+
//
91+
// Assets can be staged under different base prefixes depending on the job and
92+
// execution mode: the agent-output download directory (parent of
93+
// agent_output.json), RUNNER_TEMP/gh-aw, or the canonical /tmp/gh-aw. The
94+
// producer (the upload_asset MCP handler) and this consumer job do not always
95+
// agree on the prefix — for example RUNNER_TEMP is /home/runner/work/_temp on
96+
// GitHub-hosted runners but the artifact is downloaded to /tmp/gh-aw. Searching
97+
// every candidate makes asset resolution robust to these path-prefix
98+
// mismatches instead of failing the whole job.
9399
const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
94-
const baseDir = agentOutputFile ? path.dirname(agentOutputFile) : path.join(process.env.RUNNER_TEMP || "/tmp", "gh-aw");
95-
const assetsDir = path.join(baseDir, "safeoutputs", "assets");
100+
const candidateBaseDirs = [];
101+
if (agentOutputFile) {
102+
candidateBaseDirs.push(path.dirname(agentOutputFile));
103+
}
104+
if (process.env.RUNNER_TEMP) {
105+
candidateBaseDirs.push(path.join(process.env.RUNNER_TEMP, "gh-aw"));
106+
}
107+
candidateBaseDirs.push("/tmp/gh-aw");
108+
// Build the per-directory assets paths, de-duplicated while preserving order.
109+
const assetsDirs = [...new Set(candidateBaseDirs.map(dir => path.join(dir, "safeoutputs", "assets")))];
110+
core.info(`Searching for staged assets in: ${assetsDirs.join(", ")}`);
96111
let uploadCount = 0;
97112
let missingAssetCount = 0;
98113
let hasChanges = false;
@@ -130,10 +145,17 @@ async function main() {
130145
return;
131146
}
132147

133-
// Check if file exists in artifacts
134-
const assetSourcePath = path.join(assetsDir, fileName);
135-
if (!fs.existsSync(assetSourcePath)) {
136-
core.warning(`${ERR_SYSTEM}: Asset file not found: ${assetSourcePath} — skipping`);
148+
// Check if file exists in any of the candidate staging directories
149+
let assetSourcePath = null;
150+
for (const dir of assetsDirs) {
151+
const candidate = path.join(dir, fileName);
152+
if (fs.existsSync(candidate)) {
153+
assetSourcePath = candidate;
154+
break;
155+
}
156+
}
157+
if (!assetSourcePath) {
158+
core.warning(`${ERR_SYSTEM}: Asset file not found in any staging directory (${assetsDirs.join(", ")}) for ${fileName} — skipping`);
137159
missingAssetCount++;
138160
continue;
139161
}

actions/setup/js/upload_assets.test.cjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,5 +175,38 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), notice: vi.fn(), warning: vi.f
175175
fs.existsSync(path.join(process.cwd(), presentTargetFile)) && fs.unlinkSync(path.join(process.cwd(), presentTargetFile));
176176
});
177177
});
178+
describe("staging directory resolution", () => {
179+
it("should find assets staged under RUNNER_TEMP when agent output dir differs", async () => {
180+
process.env.GH_AW_ASSETS_BRANCH = "assets/test-workflow";
181+
process.env.GH_AW_SAFE_OUTPUTS_STAGED = "false";
182+
// Stage the asset under a RUNNER_TEMP-based directory, NOT under the
183+
// agent-output directory (tempBase), to simulate a path-prefix mismatch.
184+
const runnerTempBase = fs.mkdtempSync(path.join("/tmp", "test-gh-aw-rt-"));
185+
process.env.RUNNER_TEMP = runnerTempBase;
186+
const runnerAssetsDir = path.join(runnerTempBase, "gh-aw", "safeoutputs", "assets");
187+
fs.mkdirSync(runnerAssetsDir, { recursive: !0 });
188+
const assetSourcePath = path.join(runnerAssetsDir, "chart.png");
189+
fs.writeFileSync(assetSourcePath, "chart content");
190+
const crypto = require("crypto"),
191+
fileContent = fs.readFileSync(assetSourcePath),
192+
targetFile = "chart-uploaded.png";
193+
setAgentOutput({
194+
items: [{ type: "upload_asset", fileName: "chart.png", sha: crypto.createHash("sha256").update(fileContent).digest("hex"), size: fileContent.length, targetFileName: targetFile, url: "https://example.com/chart.png" }],
195+
});
196+
mockExec.exec.mockImplementation(async (command, args) => {
197+
const fullCommand = Array.isArray(args) ? `${command} ${args.join(" ")}` : command;
198+
if (fullCommand.includes("rev-parse")) throw new Error("Branch does not exist");
199+
return 0;
200+
});
201+
await executeScript();
202+
expect(mockCore.setFailed).not.toHaveBeenCalled();
203+
const uploadCountCall = mockCore.setOutput.mock.calls.find(call => "upload_count" === call[0]);
204+
expect(uploadCountCall).toBeDefined();
205+
uploadCountCall && expect(uploadCountCall[1]).toBe("1");
206+
delete process.env.RUNNER_TEMP;
207+
fs.existsSync(runnerTempBase) && fs.rmSync(runnerTempBase, { recursive: !0, force: !0 });
208+
fs.existsSync(path.join(process.cwd(), targetFile)) && fs.unlinkSync(path.join(process.cwd(), targetFile));
209+
});
210+
});
178211
}));
179212
}));

0 commit comments

Comments
 (0)