Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion actions/setup/js/send_otlp_span.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1648,7 +1648,7 @@ async function sendJobConclusionSpan(spanName, options = {}) {
const isAgentCancelled = agentConclusion === "cancelled";
const isAgentNonOK = isAgentFailure || isAgentCancelled;
// STATUS_CODE_ERROR = 2, STATUS_CODE_OK = 1
const statusCode = isAgentNonOK ? 2 : 1;
let statusCode = isAgentNonOK ? 2 : 1;
let statusMessage;
if (isAgentFailure) {
statusMessage = `agent ${agentConclusion}`;
Expand All @@ -1675,6 +1675,15 @@ async function sendJobConclusionSpan(spanName, options = {}) {
runStatus = "failure";
}

// When GH_AW_AGENT_CONCLUSION and workflowRunConclusion are both absent (e.g. in the
// agent job's own post-step where needs.<job>.result is not yet visible), fall back to
// observable failure evidence so gh-aw.run.status and status.code are accurate.
if (!rawRunStatus && outputErrors.length > 0) {
runStatus = "failure";
statusCode = 2;
statusMessage = (errorMessages.length > 0 ? `errors detected: ${errorMessages[0]}` : "errors detected").slice(0, 256);
}

if (isAgentFailure && errorMessages.length > 0) {
statusMessage = `agent ${agentConclusion}: ${errorMessages[0]}`.slice(0, 256);
}
Expand Down
104 changes: 104 additions & 0 deletions actions/setup/js/send_otlp_span.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4292,6 +4292,110 @@ describe("sendJobConclusionSpan", () => {
});
});

describe("run.status fallback from observable error signals", () => {
let readFileSpy;

beforeEach(() => {
readFileSpy = vi.spyOn(fs, "readFileSync").mockImplementation(() => {
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
});
});

afterEach(() => {
readFileSpy.mockRestore();
});

it("sets gh-aw.run.status=failure and STATUS_CODE_ERROR when conclusion is absent but outputErrors exist", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);

process.env.GH_AW_OTLP_ENDPOINTS = JSON.stringify([{ url: "https://traces.example.com" }]);
// GH_AW_AGENT_CONCLUSION intentionally not set (simulates agent job post-step)

readFileSpy.mockImplementation(filePath => {
if (filePath === "/tmp/gh-aw/agent_output.json") {
return JSON.stringify({ errors: [{ message: "push_to_pull_request_branch:push failed" }] });
}
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
});

await sendJobConclusionSpan("gh-aw.job.conclusion");

const body = JSON.parse(mockFetch.mock.calls[0][1].body);
const span = body.resourceSpans[0].scopeSpans[0].spans[0];
const attrs = Object.fromEntries(span.attributes.map(a => [a.key, a.value.intValue ?? a.value.stringValue ?? a.value.boolValue]));
expect(attrs["gh-aw.run.status"]).toBe("failure");
expect(span.status.code).toBe(2);
expect(span.status.message).toBe("errors detected: push_to_pull_request_branch:push failed");
});

it("includes the first error message in statusMessage when conclusion is absent", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);

process.env.GH_AW_OTLP_ENDPOINTS = JSON.stringify([{ url: "https://traces.example.com" }]);

readFileSpy.mockImplementation(filePath => {
if (filePath === "/tmp/gh-aw/agent_output.json") {
return JSON.stringify({ errors: [{ message: "Rate limit exceeded" }, { message: "second error" }] });
}
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
});

await sendJobConclusionSpan("gh-aw.job.conclusion");

const body = JSON.parse(mockFetch.mock.calls[0][1].body);
const span = body.resourceSpans[0].scopeSpans[0].spans[0];
expect(span.status.message).toBe("errors detected: Rate limit exceeded");
});

it("does not override explicit GH_AW_AGENT_CONCLUSION=success even when outputErrors exist", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);

process.env.GH_AW_OTLP_ENDPOINTS = JSON.stringify([{ url: "https://traces.example.com" }]);
process.env.GH_AW_AGENT_CONCLUSION = "success";

readFileSpy.mockImplementation(filePath => {
if (filePath === "/tmp/gh-aw/agent_output.json") {
return JSON.stringify({ errors: [{ message: "non-fatal warning error" }] });
}
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
});

await sendJobConclusionSpan("gh-aw.job.conclusion");

const body = JSON.parse(mockFetch.mock.calls[0][1].body);
const span = body.resourceSpans[0].scopeSpans[0].spans[0];
const attrs = Object.fromEntries(span.attributes.map(a => [a.key, a.value.intValue ?? a.value.stringValue ?? a.value.boolValue]));
expect(attrs["gh-aw.run.status"]).toBe("success");
expect(span.status.code).toBe(1);
});

it("keeps gh-aw.run.status=success and STATUS_CODE_OK when conclusion is absent and there are no outputErrors", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);

process.env.GH_AW_OTLP_ENDPOINTS = JSON.stringify([{ url: "https://traces.example.com" }]);
// No GH_AW_AGENT_CONCLUSION and no errors

readFileSpy.mockImplementation(filePath => {
if (filePath === "/tmp/gh-aw/agent_output.json") {
return JSON.stringify({ items: [{ type: "pull_request" }] });
}
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
});

await sendJobConclusionSpan("gh-aw.job.conclusion");

const body = JSON.parse(mockFetch.mock.calls[0][1].body);
const span = body.resourceSpans[0].scopeSpans[0].spans[0];
const attrs = Object.fromEntries(span.attributes.map(a => [a.key, a.value.intValue ?? a.value.stringValue ?? a.value.boolValue]));
expect(attrs["gh-aw.run.status"]).toBe("success");
expect(span.status.code).toBe(1);
});
});

describe("rate-limit enrichment in conclusion span", () => {
let readFileSpy;

Expand Down