Skip to content

Commit e9a50ed

Browse files
authored
Emit gh-aw.agent.agent span for timed-out runs when agent_output.json is missing (#27237)
1 parent 525dab3 commit e9a50ed

2 files changed

Lines changed: 38 additions & 1 deletion

File tree

actions/setup/js/send_otlp_span.cjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -829,7 +829,12 @@ async function sendJobConclusionSpan(spanName, options = {}) {
829829
try {
830830
agentEndMs = fs.statSync("/tmp/gh-aw/agent_output.json").mtimeMs;
831831
} catch {
832-
// agent_output.json may not exist for non-agent jobs; skip dedicated span.
832+
// agent_output.json may be absent for agent failures, including timed-out
833+
// runs where the process was killed before writing output. Fall back to
834+
// nowMs() so we still emit the dedicated agent span for these failures.
835+
if (isAgentFailure && jobName === "agent" && typeof agentStartMs === "number" && agentStartMs > 0) {
836+
agentEndMs = nowMs();
837+
}
833838
}
834839

835840
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "";

actions/setup/js/send_otlp_span.test.cjs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,38 @@ describe("sendJobConclusionSpan", () => {
16301630
expect(span.name).toBe("gh-aw.job.conclusion");
16311631
});
16321632

1633+
it("emits a dedicated agent span on timed_out when agent_output mtime is unavailable", async () => {
1634+
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
1635+
vi.stubGlobal("fetch", mockFetch);
1636+
1637+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://traces.example.com";
1638+
process.env.INPUT_JOB_NAME = "agent";
1639+
process.env.GH_AW_AGENT_CONCLUSION = "timed_out";
1640+
1641+
const startMs = 1_700_000_000_000;
1642+
const statSpy = vi.spyOn(fs, "statSync").mockImplementation(() => {
1643+
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
1644+
});
1645+
1646+
await sendJobConclusionSpan("gh-aw.agent.conclusion", { startMs });
1647+
1648+
statSpy.mockRestore();
1649+
expect(mockFetch).toHaveBeenCalledTimes(2);
1650+
1651+
const agentBody = JSON.parse(mockFetch.mock.calls[0][1].body);
1652+
const agentSpan = agentBody.resourceSpans[0].scopeSpans[0].spans[0];
1653+
expect(agentSpan.name).toBe("gh-aw.agent.agent");
1654+
expect(agentSpan.startTimeUnixNano).toBe(toNanoString(startMs));
1655+
expect(BigInt(agentSpan.endTimeUnixNano)).toBeGreaterThan(BigInt(toNanoString(startMs)));
1656+
1657+
const conclusionBody = JSON.parse(mockFetch.mock.calls[1][1].body);
1658+
const conclusionSpan = conclusionBody.resourceSpans[0].scopeSpans[0].spans[0];
1659+
expect(conclusionSpan.name).toBe("gh-aw.agent.conclusion");
1660+
expect(agentSpan.parentSpanId).toBe(conclusionSpan.spanId);
1661+
expect(conclusionSpan.status.code).toBe(2);
1662+
expect(conclusionSpan.status.message).toContain("agent timed_out");
1663+
});
1664+
16331665
it("does not emit a dedicated agent span for non-agent jobs", async () => {
16341666
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
16351667
vi.stubGlobal("fetch", mockFetch);

0 commit comments

Comments
 (0)