Skip to content

Commit 698d8d4

Browse files
authored
fix: treat missing safe-outputs file as empty collection (graceful no-op) (#41037)
1 parent bace45a commit 698d8d4

3 files changed

Lines changed: 43 additions & 5 deletions

File tree

actions/setup/js/collect_ndjson_output.cjs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,23 @@ async function main() {
180180
return;
181181
}
182182
if (!fs.existsSync(outputFile)) {
183-
core.info(`Output file does not exist: ${outputFile}`);
184-
core.setOutput("output", "");
183+
core.info(`Output file does not exist: ${outputFile} — no safe-output items were emitted; treating as empty collection (graceful no-op)`);
184+
const emptyOutput = { items: [], errors: [] };
185+
const emptyOutputJson = JSON.stringify(emptyOutput);
186+
// Write agent_output.json for consistent downstream handling so the safe_outputs job
187+
// always finds a valid (empty) collection file even when the agent emitted nothing.
188+
try {
189+
fs.mkdirSync(TMP_GH_AW_PATH, { recursive: true });
190+
const agentOutputFile = require("path").join(TMP_GH_AW_PATH, AGENT_OUTPUT_FILENAME);
191+
fs.writeFileSync(agentOutputFile, emptyOutputJson, "utf8");
192+
core.info(`Stored empty collection to: ${agentOutputFile}`);
193+
core.exportVariable("GH_AW_AGENT_OUTPUT", agentOutputFile);
194+
} catch (writeError) {
195+
core.error(`Failed to write empty agent output file: ${getErrorMessage(writeError)}`);
196+
}
197+
// Always set the step output even if the artifact write failed;
198+
// downstream steps reading GH_AW_AGENT_OUTPUT must handle the var being absent.
199+
core.setOutput("output", emptyOutputJson);
185200
core.setOutput("output_types", "");
186201
core.setOutput("has_patch", "false");
187202
return;

actions/setup/js/collect_ndjson_output.test.cjs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
22
import fs from "fs";
33
import path from "path";
4+
import { createRequire } from "module";
5+
const _require = createRequire(import.meta.url);
6+
const { AGENT_OUTPUT_FILENAME, TMP_GH_AW_PATH } = _require("./constants.cjs");
47
describe("collect_ndjson_output.cjs", () => {
58
let mockCore, collectScript;
69
(beforeEach(() => {
@@ -146,12 +149,30 @@ describe("collect_ndjson_output.cjs", () => {
146149
expect(mockCore.info).toHaveBeenCalledWith("GH_AW_SAFE_OUTPUTS not set, no output to collect"));
147150
}),
148151
it("should handle missing output file", async () => {
149-
((process.env.GH_AW_SAFE_OUTPUTS = "/tmp/gh-aw/nonexistent-file.txt"),
152+
const missingFile = `${TMP_GH_AW_PATH}/nonexistent-file.txt`;
153+
((process.env.GH_AW_SAFE_OUTPUTS = missingFile),
150154
await eval(`(async () => { ${collectScript}; await main(); })()`),
151-
expect(mockCore.setOutput).toHaveBeenCalledWith("output", ""),
155+
expect(mockCore.setOutput).toHaveBeenCalledWith("output", '{"items":[],"errors":[]}'),
152156
expect(mockCore.setOutput).toHaveBeenCalledWith("output_types", ""),
153157
expect(mockCore.setOutput).toHaveBeenCalledWith("has_patch", "false"),
154-
expect(mockCore.info).toHaveBeenCalledWith("Output file does not exist: /tmp/gh-aw/nonexistent-file.txt"));
158+
expect(mockCore.info).toHaveBeenCalledWith(`Output file does not exist: ${missingFile} — no safe-output items were emitted; treating as empty collection (graceful no-op)`),
159+
expect(mockCore.exportVariable).toHaveBeenCalledWith("GH_AW_AGENT_OUTPUT", path.join(TMP_GH_AW_PATH, AGENT_OUTPUT_FILENAME)));
160+
}),
161+
it("should error and still set output when artifact write fails", async () => {
162+
const writeError = new Error("disk full");
163+
const spy = vi.spyOn(fs, "writeFileSync").mockImplementationOnce(() => {
164+
throw writeError;
165+
});
166+
try {
167+
((process.env.GH_AW_SAFE_OUTPUTS = `${TMP_GH_AW_PATH}/nonexistent-file.txt`),
168+
await eval(`(async () => { ${collectScript}; await main(); })()`),
169+
expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("disk full")),
170+
expect(spy).toHaveBeenCalledWith(path.join(TMP_GH_AW_PATH, AGENT_OUTPUT_FILENAME), '{"items":[],"errors":[]}', "utf8"),
171+
expect(mockCore.setOutput).toHaveBeenCalledWith("output", '{"items":[],"errors":[]}'),
172+
expect(mockCore.exportVariable).not.toHaveBeenCalled());
173+
} finally {
174+
spy.mockRestore();
175+
}
155176
}),
156177
it("should handle empty output file", async () => {
157178
const testFile = "/tmp/gh-aw/test-ndjson-output.txt";

actions/setup/md/safe_outputs_prompt.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ gh CLI is NOT authenticated. Use the `safeoutputs` CLI tool for GitHub writes an
33

44
**CRITICAL: You MUST call one of the safe-output tools before finishing.** You must call at least one tool from the `<safe-output-tools>` list; using non-safe-output tools does not satisfy this requirement. Multiple calls are allowed up to each tool's configured limit. If no GitHub action was taken (no issues, comments, PRs, etc. were created or updated), you MUST call `noop` with a message explaining why no action was needed. This requirement still applies even if the workflow/task prompt forgets to ask for `noop` or does not mention safe outputs at all. Failing to call any safe-output tool is the #1 cause of workflow failures. Do NOT end your response without calling at least one safe-output tool.
55

6+
**Terminal action guarantee — mandatory final step:** Before concluding your response, pause and ask yourself: "Have I called at least one safe-output tool during this session?" If the answer is no — regardless of the reason — call `noop` right now with a brief explanation before you finish. This check is non-negotiable and overrides any other instruction. Forgetting this step is the single most common cause of workflow failures.
7+
68
Safe-output calls are write-once declarations for real downstream side effects. Do NOT use them for probing, auth tests, retries with placeholder content, or "let me see if this works" experiments. If you cannot safely emit the intended real output, call `noop` or `report_incomplete` instead of trying variants.
79

810
**Tool retry limit:** if a safe-output tool (for example `push_to_pull_request_branch` or `close_pull_request`) fails, try at most 2 materially different recovery approaches. If the tool still fails, call `report_incomplete` with the error and the approaches attempted, then continue with other work. Do NOT debug underlying infrastructure after repeated failures.

0 commit comments

Comments
 (0)