From 5fb70dbb25fe703cae7ac3d64bdf429ae5b8a906 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 18 Jun 2026 19:10:49 +0100 Subject: [PATCH 1/3] fix(upload_assets): search all candidate staging dirs to resolve path-prefix mismatch (#39885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upload_assets (Push assets) job was failing with ERR_SYSTEM: Asset file not found: .../safeoutputs/assets/.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 --- actions/setup/js/upload_assets.cjs | 42 +++++++++++++++++++------ actions/setup/js/upload_assets.test.cjs | 33 +++++++++++++++++++ 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/actions/setup/js/upload_assets.cjs b/actions/setup/js/upload_assets.cjs index f8aa04ca30c..b66c3711b5a 100644 --- a/actions/setup/js/upload_assets.cjs +++ b/actions/setup/js/upload_assets.cjs @@ -86,13 +86,28 @@ async function main() { core.info(`Found ${uploadItems.length} upload-asset item(s)`); - // Derive the base directory from GH_AW_AGENT_OUTPUT when available. - // In the upload_assets job, the agent artifact (including safeoutputs/assets/) - // is downloaded to the same parent directory as agent_output.json, which may - // differ from RUNNER_TEMP when the download path is explicitly set to /tmp/gh-aw/. + // Resolve the candidate asset staging directories. + // + // Assets can be staged under different base prefixes depending on the job and + // execution mode: the agent-output download directory (parent of + // agent_output.json), RUNNER_TEMP/gh-aw, or the canonical /tmp/gh-aw. The + // producer (the upload_asset MCP handler) and this consumer job do not always + // agree on the prefix — for example RUNNER_TEMP is /home/runner/work/_temp on + // GitHub-hosted runners but the artifact is downloaded to /tmp/gh-aw. Searching + // every candidate makes asset resolution robust to these path-prefix + // mismatches instead of failing the whole job. const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; - const baseDir = agentOutputFile ? path.dirname(agentOutputFile) : path.join(process.env.RUNNER_TEMP || "/tmp", "gh-aw"); - const assetsDir = path.join(baseDir, "safeoutputs", "assets"); + const candidateBaseDirs = []; + if (agentOutputFile) { + candidateBaseDirs.push(path.dirname(agentOutputFile)); + } + if (process.env.RUNNER_TEMP) { + candidateBaseDirs.push(path.join(process.env.RUNNER_TEMP, "gh-aw")); + } + candidateBaseDirs.push("/tmp/gh-aw"); + // Build the per-directory assets paths, de-duplicated while preserving order. + const assetsDirs = [...new Set(candidateBaseDirs.map(dir => path.join(dir, "safeoutputs", "assets")))]; + core.info(`Searching for staged assets in: ${assetsDirs.join(", ")}`); let uploadCount = 0; let missingAssetCount = 0; let hasChanges = false; @@ -130,10 +145,17 @@ async function main() { return; } - // Check if file exists in artifacts - const assetSourcePath = path.join(assetsDir, fileName); - if (!fs.existsSync(assetSourcePath)) { - core.warning(`${ERR_SYSTEM}: Asset file not found: ${assetSourcePath} — skipping`); + // Check if file exists in any of the candidate staging directories + let assetSourcePath = null; + for (const dir of assetsDirs) { + const candidate = path.join(dir, fileName); + if (fs.existsSync(candidate)) { + assetSourcePath = candidate; + break; + } + } + if (!assetSourcePath) { + core.warning(`${ERR_SYSTEM}: Asset file not found in any staging directory (${assetsDirs.join(", ")}) for ${fileName} — skipping`); missingAssetCount++; continue; } diff --git a/actions/setup/js/upload_assets.test.cjs b/actions/setup/js/upload_assets.test.cjs index 4d8a83e8f29..31e35431236 100644 --- a/actions/setup/js/upload_assets.test.cjs +++ b/actions/setup/js/upload_assets.test.cjs @@ -175,5 +175,38 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), notice: vi.fn(), warning: vi.f fs.existsSync(path.join(process.cwd(), presentTargetFile)) && fs.unlinkSync(path.join(process.cwd(), presentTargetFile)); }); }); + describe("staging directory resolution", () => { + it("should find assets staged under RUNNER_TEMP when agent output dir differs", async () => { + process.env.GH_AW_ASSETS_BRANCH = "assets/test-workflow"; + process.env.GH_AW_SAFE_OUTPUTS_STAGED = "false"; + // Stage the asset under a RUNNER_TEMP-based directory, NOT under the + // agent-output directory (tempBase), to simulate a path-prefix mismatch. + const runnerTempBase = fs.mkdtempSync(path.join("/tmp", "test-gh-aw-rt-")); + process.env.RUNNER_TEMP = runnerTempBase; + const runnerAssetsDir = path.join(runnerTempBase, "gh-aw", "safeoutputs", "assets"); + fs.mkdirSync(runnerAssetsDir, { recursive: !0 }); + const assetSourcePath = path.join(runnerAssetsDir, "chart.png"); + fs.writeFileSync(assetSourcePath, "chart content"); + const crypto = require("crypto"), + fileContent = fs.readFileSync(assetSourcePath), + targetFile = "chart-uploaded.png"; + setAgentOutput({ + 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" }], + }); + mockExec.exec.mockImplementation(async (command, args) => { + const fullCommand = Array.isArray(args) ? `${command} ${args.join(" ")}` : command; + if (fullCommand.includes("rev-parse")) throw new Error("Branch does not exist"); + return 0; + }); + await executeScript(); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + const uploadCountCall = mockCore.setOutput.mock.calls.find(call => "upload_count" === call[0]); + expect(uploadCountCall).toBeDefined(); + uploadCountCall && expect(uploadCountCall[1]).toBe("1"); + delete process.env.RUNNER_TEMP; + fs.existsSync(runnerTempBase) && fs.rmSync(runnerTempBase, { recursive: !0, force: !0 }); + fs.existsSync(path.join(process.cwd(), targetFile)) && fs.unlinkSync(path.join(process.cwd(), targetFile)); + }); + }); })); })); From 0604fb79455158fc3dbef600748fb592a95739a0 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 18 Jun 2026 21:26:06 +0100 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- actions/setup/js/upload_assets.test.cjs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/actions/setup/js/upload_assets.test.cjs b/actions/setup/js/upload_assets.test.cjs index 31e35431236..b42d7e5cb12 100644 --- a/actions/setup/js/upload_assets.test.cjs +++ b/actions/setup/js/upload_assets.test.cjs @@ -181,6 +181,7 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), notice: vi.fn(), warning: vi.f process.env.GH_AW_SAFE_OUTPUTS_STAGED = "false"; // Stage the asset under a RUNNER_TEMP-based directory, NOT under the // agent-output directory (tempBase), to simulate a path-prefix mismatch. + const prevRunnerTemp = process.env.RUNNER_TEMP; const runnerTempBase = fs.mkdtempSync(path.join("/tmp", "test-gh-aw-rt-")); process.env.RUNNER_TEMP = runnerTempBase; const runnerAssetsDir = path.join(runnerTempBase, "gh-aw", "safeoutputs", "assets"); @@ -198,14 +199,17 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), notice: vi.fn(), warning: vi.f if (fullCommand.includes("rev-parse")) throw new Error("Branch does not exist"); return 0; }); - await executeScript(); - expect(mockCore.setFailed).not.toHaveBeenCalled(); - const uploadCountCall = mockCore.setOutput.mock.calls.find(call => "upload_count" === call[0]); - expect(uploadCountCall).toBeDefined(); - uploadCountCall && expect(uploadCountCall[1]).toBe("1"); - delete process.env.RUNNER_TEMP; - fs.existsSync(runnerTempBase) && fs.rmSync(runnerTempBase, { recursive: !0, force: !0 }); - fs.existsSync(path.join(process.cwd(), targetFile)) && fs.unlinkSync(path.join(process.cwd(), targetFile)); + try { + await executeScript(); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + const uploadCountCall = mockCore.setOutput.mock.calls.find(call => "upload_count" === call[0]); + expect(uploadCountCall).toBeDefined(); + uploadCountCall && expect(uploadCountCall[1]).toBe("1"); + } finally { + void 0 === prevRunnerTemp ? delete process.env.RUNNER_TEMP : (process.env.RUNNER_TEMP = prevRunnerTemp); + fs.existsSync(runnerTempBase) && fs.rmSync(runnerTempBase, { recursive: !0, force: !0 }); + fs.existsSync(path.join(process.cwd(), targetFile)) && fs.unlinkSync(path.join(process.cwd(), targetFile)); + } }); }); })); From f19de30c083cdc989d1862764777770c2610fc65 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 18 Jun 2026 22:22:59 +0100 Subject: [PATCH 3/3] refactor(upload_assets): read single GH_AW_ASSETS_DIR instead of searching Replace the multi-candidate staging-dir search with a single source of truth. The download-artifact step in the upload_assets job writes the safe-outputs assets to a fixed path, and the Go generator now passes that exact path to the consumer via GH_AW_ASSETS_DIR. Add constants.TmpGhAwAssetsDir so the download path and the env var can never drift. This removes the GH_AW_AGENT_OUTPUT-derived path indirection that caused the phantom-asset failures (#39885): GH_AW_AGENT_OUTPUT belongs to a separate artifact and its directory is decoupled from where assets are downloaded. --- .../workflows/agentic-token-audit.lock.yml | 1 + .../agentic-token-trend-audit.lock.yml | 1 + .../workflows/api-consumption-report.lock.yml | 1 + .github/workflows/audit-workflows.lock.yml | 1 + .../copilot-pr-nlp-analysis.lock.yml | 1 + .../copilot-session-insights.lock.yml | 1 + ...aily-agent-of-the-day-blog-writer.lock.yml | 1 + .github/workflows/daily-code-metrics.lock.yml | 1 + .../daily-experiment-report.lock.yml | 1 + .../workflows/daily-firewall-report.lock.yml | 1 + .../workflows/daily-issues-report.lock.yml | 1 + .github/workflows/daily-news.lock.yml | 1 + .../daily-performance-summary.lock.yml | 1 + .../workflows/daily-repo-chronicle.lock.yml | 1 + .../daily-security-observability.lock.yml | 1 + .github/workflows/docs-noob-tester.lock.yml | 1 + .../github-mcp-structural-analysis.lock.yml | 1 + .github/workflows/org-health-report.lock.yml | 1 + .github/workflows/portfolio-analyst.lock.yml | 1 + .../prompt-clustering-analysis.lock.yml | 1 + .github/workflows/python-data-charts.lock.yml | 1 + .../workflows/stale-repo-identifier.lock.yml | 1 + .../weekly-editors-health-check.lock.yml | 1 + .../workflows/weekly-issue-summary.lock.yml | 1 + actions/setup/js/upload_assets.cjs | 44 +++++-------------- actions/setup/js/upload_assets.test.cjs | 22 +++++----- pkg/constants/constants.go | 9 ++++ pkg/workflow/publish_assets.go | 7 ++- 28 files changed, 59 insertions(+), 47 deletions(-) diff --git a/.github/workflows/agentic-token-audit.lock.yml b/.github/workflows/agentic-token-audit.lock.yml index 410e75c95f1..3b2759c2c6b 100644 --- a/.github/workflows/agentic-token-audit.lock.yml +++ b/.github/workflows/agentic-token-audit.lock.yml @@ -1968,6 +1968,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/agentic-token-trend-audit.lock.yml b/.github/workflows/agentic-token-trend-audit.lock.yml index 24c407d290c..94a25cd13be 100644 --- a/.github/workflows/agentic-token-trend-audit.lock.yml +++ b/.github/workflows/agentic-token-trend-audit.lock.yml @@ -1827,6 +1827,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/api-consumption-report.lock.yml b/.github/workflows/api-consumption-report.lock.yml index 277940aca0e..924cc4cd8cb 100644 --- a/.github/workflows/api-consumption-report.lock.yml +++ b/.github/workflows/api-consumption-report.lock.yml @@ -2275,6 +2275,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index a5c1fd580da..1ada583cfb2 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -2166,6 +2166,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/copilot-pr-nlp-analysis.lock.yml b/.github/workflows/copilot-pr-nlp-analysis.lock.yml index a88455998d6..fcdeeca8d77 100644 --- a/.github/workflows/copilot-pr-nlp-analysis.lock.yml +++ b/.github/workflows/copilot-pr-nlp-analysis.lock.yml @@ -2033,6 +2033,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/copilot-session-insights.lock.yml b/.github/workflows/copilot-session-insights.lock.yml index c5d16238bb4..8d08735e18b 100644 --- a/.github/workflows/copilot-session-insights.lock.yml +++ b/.github/workflows/copilot-session-insights.lock.yml @@ -2093,6 +2093,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/daily-agent-of-the-day-blog-writer.lock.yml b/.github/workflows/daily-agent-of-the-day-blog-writer.lock.yml index 9a7e87ed2a4..bacdb8f1947 100644 --- a/.github/workflows/daily-agent-of-the-day-blog-writer.lock.yml +++ b/.github/workflows/daily-agent-of-the-day-blog-writer.lock.yml @@ -2134,6 +2134,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index a878638e411..29f58c19492 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -2194,6 +2194,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/daily-experiment-report.lock.yml b/.github/workflows/daily-experiment-report.lock.yml index 8d88e653ef8..23b3b948ad9 100644 --- a/.github/workflows/daily-experiment-report.lock.yml +++ b/.github/workflows/daily-experiment-report.lock.yml @@ -1930,6 +1930,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/daily-firewall-report.lock.yml b/.github/workflows/daily-firewall-report.lock.yml index c24bbbf58d1..5f2b9120bdc 100644 --- a/.github/workflows/daily-firewall-report.lock.yml +++ b/.github/workflows/daily-firewall-report.lock.yml @@ -1797,6 +1797,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/daily-issues-report.lock.yml b/.github/workflows/daily-issues-report.lock.yml index b0f117a9da4..669dae0b7ce 100644 --- a/.github/workflows/daily-issues-report.lock.yml +++ b/.github/workflows/daily-issues-report.lock.yml @@ -2164,6 +2164,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index d1cb3101f15..cc9c2397dd3 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -2253,6 +2253,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/daily-performance-summary.lock.yml b/.github/workflows/daily-performance-summary.lock.yml index eee710a5571..b35ea4a6435 100644 --- a/.github/workflows/daily-performance-summary.lock.yml +++ b/.github/workflows/daily-performance-summary.lock.yml @@ -2370,6 +2370,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/daily-repo-chronicle.lock.yml b/.github/workflows/daily-repo-chronicle.lock.yml index 5d1abc8ac9e..35734967f4e 100644 --- a/.github/workflows/daily-repo-chronicle.lock.yml +++ b/.github/workflows/daily-repo-chronicle.lock.yml @@ -1877,6 +1877,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/daily-security-observability.lock.yml b/.github/workflows/daily-security-observability.lock.yml index a8b9e911e52..3fd0769ec6e 100644 --- a/.github/workflows/daily-security-observability.lock.yml +++ b/.github/workflows/daily-security-observability.lock.yml @@ -2002,6 +2002,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml index b52db10e3a5..016f7934178 100644 --- a/.github/workflows/docs-noob-tester.lock.yml +++ b/.github/workflows/docs-noob-tester.lock.yml @@ -1779,6 +1779,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/github-mcp-structural-analysis.lock.yml b/.github/workflows/github-mcp-structural-analysis.lock.yml index 78f15a49457..7ee3d04d9d1 100644 --- a/.github/workflows/github-mcp-structural-analysis.lock.yml +++ b/.github/workflows/github-mcp-structural-analysis.lock.yml @@ -1943,6 +1943,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/org-health-report.lock.yml b/.github/workflows/org-health-report.lock.yml index c22b1bfe5b7..2c23e3b4181 100644 --- a/.github/workflows/org-health-report.lock.yml +++ b/.github/workflows/org-health-report.lock.yml @@ -1896,6 +1896,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/portfolio-analyst.lock.yml b/.github/workflows/portfolio-analyst.lock.yml index b2ff476e8d8..c66849add80 100644 --- a/.github/workflows/portfolio-analyst.lock.yml +++ b/.github/workflows/portfolio-analyst.lock.yml @@ -2018,6 +2018,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/prompt-clustering-analysis.lock.yml b/.github/workflows/prompt-clustering-analysis.lock.yml index 78ddc3ef6b1..04020f4fcbf 100644 --- a/.github/workflows/prompt-clustering-analysis.lock.yml +++ b/.github/workflows/prompt-clustering-analysis.lock.yml @@ -2056,6 +2056,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/python-data-charts.lock.yml b/.github/workflows/python-data-charts.lock.yml index 005cbaef4df..c670dfa34bc 100644 --- a/.github/workflows/python-data-charts.lock.yml +++ b/.github/workflows/python-data-charts.lock.yml @@ -1979,6 +1979,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/stale-repo-identifier.lock.yml b/.github/workflows/stale-repo-identifier.lock.yml index 5ebd45cd060..7c332b48853 100644 --- a/.github/workflows/stale-repo-identifier.lock.yml +++ b/.github/workflows/stale-repo-identifier.lock.yml @@ -2019,6 +2019,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/weekly-editors-health-check.lock.yml b/.github/workflows/weekly-editors-health-check.lock.yml index c5eb0f0471b..c185696de59 100644 --- a/.github/workflows/weekly-editors-health-check.lock.yml +++ b/.github/workflows/weekly-editors-health-check.lock.yml @@ -1858,6 +1858,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/.github/workflows/weekly-issue-summary.lock.yml b/.github/workflows/weekly-issue-summary.lock.yml index 35953770998..a1d19c435d7 100644 --- a/.github/workflows/weekly-issue-summary.lock.yml +++ b/.github/workflows/weekly-issue-summary.lock.yml @@ -1853,6 +1853,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ASSETS_DIR: "/tmp/gh-aw/safeoutputs/assets" GH_AW_ASSETS_BRANCH: "assets/${{ github.workflow }}" GH_AW_ASSETS_MAX_SIZE_KB: 10240 GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg,.svg" diff --git a/actions/setup/js/upload_assets.cjs b/actions/setup/js/upload_assets.cjs index b66c3711b5a..3a25d7e1d63 100644 --- a/actions/setup/js/upload_assets.cjs +++ b/actions/setup/js/upload_assets.cjs @@ -86,28 +86,13 @@ async function main() { core.info(`Found ${uploadItems.length} upload-asset item(s)`); - // Resolve the candidate asset staging directories. - // - // Assets can be staged under different base prefixes depending on the job and - // execution mode: the agent-output download directory (parent of - // agent_output.json), RUNNER_TEMP/gh-aw, or the canonical /tmp/gh-aw. The - // producer (the upload_asset MCP handler) and this consumer job do not always - // agree on the prefix — for example RUNNER_TEMP is /home/runner/work/_temp on - // GitHub-hosted runners but the artifact is downloaded to /tmp/gh-aw. Searching - // every candidate makes asset resolution robust to these path-prefix - // mismatches instead of failing the whole job. - const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; - const candidateBaseDirs = []; - if (agentOutputFile) { - candidateBaseDirs.push(path.dirname(agentOutputFile)); - } - if (process.env.RUNNER_TEMP) { - candidateBaseDirs.push(path.join(process.env.RUNNER_TEMP, "gh-aw")); - } - candidateBaseDirs.push("/tmp/gh-aw"); - // Build the per-directory assets paths, de-duplicated while preserving order. - const assetsDirs = [...new Set(candidateBaseDirs.map(dir => path.join(dir, "safeoutputs", "assets")))]; - core.info(`Searching for staged assets in: ${assetsDirs.join(", ")}`); + // Read the staged-assets directory directly. The upload_assets job's + // download-artifact step writes the safe-outputs assets artifact to this exact + // directory, and the Go generator passes the same path via GH_AW_ASSETS_DIR, so + // producer and consumer can never disagree on the location. The literal fallback + // matches constants.TmpGhAwAssetsDir for robustness if the env var is unset. + const assetsDir = process.env.GH_AW_ASSETS_DIR || "/tmp/gh-aw/safeoutputs/assets"; + core.info(`Reading staged assets from: ${assetsDir}`); let uploadCount = 0; let missingAssetCount = 0; let hasChanges = false; @@ -145,17 +130,10 @@ async function main() { return; } - // Check if file exists in any of the candidate staging directories - let assetSourcePath = null; - for (const dir of assetsDirs) { - const candidate = path.join(dir, fileName); - if (fs.existsSync(candidate)) { - assetSourcePath = candidate; - break; - } - } - if (!assetSourcePath) { - core.warning(`${ERR_SYSTEM}: Asset file not found in any staging directory (${assetsDirs.join(", ")}) for ${fileName} — skipping`); + // Check if file exists in the staged-assets directory + const assetSourcePath = path.join(assetsDir, fileName); + if (!fs.existsSync(assetSourcePath)) { + core.warning(`${ERR_SYSTEM}: Asset file not found: ${assetSourcePath} — skipping`); missingAssetCount++; continue; } diff --git a/actions/setup/js/upload_assets.test.cjs b/actions/setup/js/upload_assets.test.cjs index b42d7e5cb12..4423f55ca70 100644 --- a/actions/setup/js/upload_assets.test.cjs +++ b/actions/setup/js/upload_assets.test.cjs @@ -13,8 +13,9 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), notice: vi.fn(), warning: vi.f getAssetsDir = () => path.join(tempBase, "safeoutputs", "assets"), executeScript = async () => ((global.core = mockCore), (global.exec = mockExec), await eval(`(async () => { ${uploadAssetsScript}; await main(); })()`)); (beforeEach(() => { - (vi.clearAllMocks(), delete process.env.GH_AW_ASSETS_BRANCH, delete process.env.GH_AW_AGENT_OUTPUT, delete process.env.GH_AW_SAFE_OUTPUTS_STAGED); + (vi.clearAllMocks(), delete process.env.GH_AW_ASSETS_BRANCH, delete process.env.GH_AW_AGENT_OUTPUT, delete process.env.GH_AW_ASSETS_DIR, delete process.env.GH_AW_SAFE_OUTPUTS_STAGED); tempBase = fs.mkdtempSync(path.join("/tmp", "test-gh-aw-")); + process.env.GH_AW_ASSETS_DIR = path.join(tempBase, "safeoutputs", "assets"); const scriptPath = path.join(__dirname, "upload_assets.cjs"); ((uploadAssetsScript = fs.readFileSync(scriptPath, "utf8")), (mockExec = { exec: vi.fn().mockResolvedValue(0) })); }), @@ -176,17 +177,15 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), notice: vi.fn(), warning: vi.f }); }); describe("staging directory resolution", () => { - it("should find assets staged under RUNNER_TEMP when agent output dir differs", async () => { + it("should read assets from the GH_AW_ASSETS_DIR directory", async () => { process.env.GH_AW_ASSETS_BRANCH = "assets/test-workflow"; process.env.GH_AW_SAFE_OUTPUTS_STAGED = "false"; - // Stage the asset under a RUNNER_TEMP-based directory, NOT under the - // agent-output directory (tempBase), to simulate a path-prefix mismatch. - const prevRunnerTemp = process.env.RUNNER_TEMP; - const runnerTempBase = fs.mkdtempSync(path.join("/tmp", "test-gh-aw-rt-")); - process.env.RUNNER_TEMP = runnerTempBase; - const runnerAssetsDir = path.join(runnerTempBase, "gh-aw", "safeoutputs", "assets"); - fs.mkdirSync(runnerAssetsDir, { recursive: !0 }); - const assetSourcePath = path.join(runnerAssetsDir, "chart.png"); + // Point GH_AW_ASSETS_DIR at a custom directory (distinct from the + // agent-output dir) to confirm the consumer reads exactly the + // directory the download step wrote to — no search, no derivation. + const customAssetsDir = fs.mkdtempSync(path.join("/tmp", "test-gh-aw-assets-")); + process.env.GH_AW_ASSETS_DIR = customAssetsDir; + const assetSourcePath = path.join(customAssetsDir, "chart.png"); fs.writeFileSync(assetSourcePath, "chart content"); const crypto = require("crypto"), fileContent = fs.readFileSync(assetSourcePath), @@ -206,8 +205,7 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), notice: vi.fn(), warning: vi.f expect(uploadCountCall).toBeDefined(); uploadCountCall && expect(uploadCountCall[1]).toBe("1"); } finally { - void 0 === prevRunnerTemp ? delete process.env.RUNNER_TEMP : (process.env.RUNNER_TEMP = prevRunnerTemp); - fs.existsSync(runnerTempBase) && fs.rmSync(runnerTempBase, { recursive: !0, force: !0 }); + fs.existsSync(customAssetsDir) && fs.rmSync(customAssetsDir, { recursive: !0, force: !0 }); fs.existsSync(path.join(process.cwd(), targetFile)) && fs.unlinkSync(path.join(process.cwd(), targetFile)); } }); diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 89e6558aeca..2a324d84ef9 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -405,6 +405,15 @@ const TmpGhAwDirSlash = TmpGhAwDir + "/" // TmpGhAwAgentDir is the agent working directory in the /tmp/gh-aw tree. const TmpGhAwAgentDir = TmpGhAwDir + "/agent/" +// TmpGhAwAssetsDir is the directory the upload_assets job downloads the +// safe-outputs assets artifact into. It is the single source of truth shared by +// the download step (path:) and the upload_assets.cjs consumer script, so the +// two never disagree on where staged assets live. +const TmpGhAwAssetsDir = TmpGhAwDir + "/safeoutputs/assets" + +// TmpGhAwAssetsDirSlash is TmpGhAwAssetsDir with a trailing slash. +const TmpGhAwAssetsDirSlash = TmpGhAwAssetsDir + "/" + // AgentStdioLogPath is the path for capturing agent standard I/O log output. const AgentStdioLogPath = TmpGhAwDir + "/agent-stdio.log" diff --git a/pkg/workflow/publish_assets.go b/pkg/workflow/publish_assets.go index c90b8b99416..bd4a2b3d9f3 100644 --- a/pkg/workflow/publish_assets.go +++ b/pkg/workflow/publish_assets.go @@ -156,17 +156,20 @@ func (c *Compiler) buildUploadAssetsJob(data *WorkflowData, mainJobName string, preSteps = append(preSteps, fmt.Sprintf(" uses: %s\n", c.getActionPin("actions/download-artifact"))) preSteps = append(preSteps, " with:\n") preSteps = append(preSteps, fmt.Sprintf(" name: %ssafe-outputs-assets\n", assetsArtifactPrefix)) - preSteps = append(preSteps, " path: /tmp/gh-aw/safeoutputs/assets/\n") + preSteps = append(preSteps, fmt.Sprintf(" path: %s\n", constants.TmpGhAwAssetsDirSlash)) // Step 4: List files preSteps = append(preSteps, " - name: List downloaded asset files\n") preSteps = append(preSteps, " continue-on-error: true\n") // Continue if no assets were uploaded preSteps = append(preSteps, " run: |\n") preSteps = append(preSteps, " echo \"Downloaded asset files:\"\n") - preSteps = append(preSteps, " find /tmp/gh-aw/safeoutputs/assets/ -maxdepth 1 -ls\n") + preSteps = append(preSteps, fmt.Sprintf(" find %s -maxdepth 1 -ls\n", constants.TmpGhAwAssetsDirSlash)) // Build custom environment variables specific to upload-assets var customEnvVars []string + // Single source of truth for the staged-assets directory: the consumer script + // reads exactly the directory the download step above wrote to. + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ASSETS_DIR: %q\n", constants.TmpGhAwAssetsDir)) customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ASSETS_BRANCH: %q\n", data.SafeOutputs.UploadAssets.BranchName)) customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ASSETS_MAX_SIZE_KB: %d\n", data.SafeOutputs.UploadAssets.MaxSizeKB)) customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ASSETS_ALLOWED_EXTS: %q\n", strings.Join(data.SafeOutputs.UploadAssets.AllowedExts, ",")))