From c88f19f6f37537dd8431fc6553b67552217422ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 02:05:23 +0000 Subject: [PATCH 1/3] Initial plan From 5b1b1fbbeec337efa17a5aa3a7ff8e352eae35a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 02:24:44 +0000 Subject: [PATCH 2/3] fix: enrich OTEL conclusion spans for cancelled and partial failures Agent-Logs-Url: https://github.com/github/gh-aw/sessions/4ed28949-5be5-4b74-867b-476f427741b5 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/action_otlp.test.cjs | 5 +-- actions/setup/js/send_otlp_span.cjs | 11 +++--- actions/setup/js/send_otlp_span.test.cjs | 43 ++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/actions/setup/js/action_otlp.test.cjs b/actions/setup/js/action_otlp.test.cjs index 4c323f010bb..2192fd3549a 100644 --- a/actions/setup/js/action_otlp.test.cjs +++ b/actions/setup/js/action_otlp.test.cjs @@ -336,7 +336,7 @@ describe("action_conclusion_otlp run()", () => { delete process.env.GH_AW_AGENT_CONCLUSION; }); - it("records cancelled conclusion as STATUS_CODE_OK when GH_AW_AGENT_CONCLUSION is 'cancelled'", async () => { + it("records cancelled conclusion as STATUS_CODE_ERROR when GH_AW_AGENT_CONCLUSION is 'cancelled'", async () => { process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:14317"; process.env.GH_AW_AGENT_CONCLUSION = "cancelled"; let capturedBody; @@ -349,7 +349,8 @@ describe("action_conclusion_otlp run()", () => { const payload = JSON.parse(capturedBody); const span = payload?.resourceSpans?.[0]?.scopeSpans?.[0]?.spans?.[0]; - expect(span?.status?.code).toBe(1); // STATUS_CODE_OK (cancelled is not an error) + expect(span?.status?.code).toBe(2); // STATUS_CODE_ERROR + expect(span?.status?.message).toBe("agent cancelled"); const conclusionAttr = span?.attributes?.find(a => a.key === "gh-aw.agent.conclusion"); expect(conclusionAttr?.value?.stringValue).toBe("cancelled"); fetchSpy.mockRestore(); diff --git a/actions/setup/js/send_otlp_span.cjs b/actions/setup/js/send_otlp_span.cjs index aa7c55e320a..9a65edeb2e4 100644 --- a/actions/setup/js/send_otlp_span.cjs +++ b/actions/setup/js/send_otlp_span.cjs @@ -700,11 +700,12 @@ async function sendJobConclusionSpan(spanName, options = {}) { // Values: "success", "failure", "timed_out", "cancelled", "skipped". const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || ""; - // Mark the span as an error when the agent job failed or timed out. + // Mark the span as an error when the agent job failed, timed out, or was cancelled. const isAgentFailure = agentConclusion === "failure" || agentConclusion === "timed_out"; + const isAgentCancelled = agentConclusion === "cancelled"; // STATUS_CODE_ERROR = 2, STATUS_CODE_OK = 1 - const statusCode = isAgentFailure ? 2 : 1; - let statusMessage = isAgentFailure ? `agent ${agentConclusion}` : undefined; + const statusCode = isAgentFailure || isAgentCancelled ? 2 : 1; + let statusMessage = isAgentFailure ? `agent ${agentConclusion}` : isAgentCancelled ? "agent cancelled" : undefined; // Always read agent_output.json so output metrics are available on all outcomes. const agentOutput = readJSONIfExists("/tmp/gh-aw/agent_output.json") || {}; @@ -748,7 +749,7 @@ async function sendJobConclusionSpan(spanName, options = {}) { if (agentConclusion) { attributes.push(buildAttr("gh-aw.agent.conclusion", agentConclusion)); } - if (isAgentFailure && errorMessages.length > 0) { + if (errorMessages.length > 0) { attributes.push(buildAttr("gh-aw.error.count", outputErrors.length)); attributes.push(buildAttr("gh-aw.error.messages", errorMessages.join(" | "))); } @@ -804,7 +805,7 @@ async function sendJobConclusionSpan(spanName, options = {}) { // making individual errors queryable and classifiable in backends like // Grafana Tempo, Honeycomb, and Datadog. const buildSpanEvents = eventTimeMs => { - if (!isAgentFailure) { + if (outputErrors.length === 0) { return []; } const errorTimeNano = toNanoString(eventTimeMs); diff --git a/actions/setup/js/send_otlp_span.test.cjs b/actions/setup/js/send_otlp_span.test.cjs index 402fc117e73..7a5dd0592d5 100644 --- a/actions/setup/js/send_otlp_span.test.cjs +++ b/actions/setup/js/send_otlp_span.test.cjs @@ -2123,6 +2123,34 @@ describe("sendJobConclusionSpan", () => { expect(errorMessages.value.stringValue).toBe("Rate limit exceeded | Tool call failed"); }); + it("adds gh-aw.error attributes when agent_output.json has errors on success", async () => { + const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" }); + vi.stubGlobal("fetch", mockFetch); + + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "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: "partial failure one" }, { message: "partial failure two" }] }); + } + 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 = span.attributes; + expect(span.status.code).toBe(1); + expect(attrs).toContainEqual({ key: "gh-aw.error.count", value: { intValue: 2 } }); + expect(attrs).toContainEqual({ key: "gh-aw.error.messages", value: { stringValue: "partial failure one | partial failure two" } }); + expect(span.events).toHaveLength(2); + expect(span.events[0].name).toBe("exception"); + expect(span.events[0].attributes).toContainEqual({ key: "exception.type", value: { stringValue: "gh-aw.AgentError" } }); + expect(span.events[0].attributes).toContainEqual({ key: "exception.message", value: { stringValue: "partial failure one" } }); + }); + it("enriches statusMessage with the first error message on failure", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" }); vi.stubGlobal("fetch", mockFetch); @@ -2165,6 +2193,21 @@ describe("sendJobConclusionSpan", () => { expect(span.status.message).toBe("agent timed_out: Execution exceeded 30 minute limit"); }); + it("marks cancelled conclusion spans as errors", async () => { + const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" }); + vi.stubGlobal("fetch", mockFetch); + + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://traces.example.com"; + process.env.GH_AW_AGENT_CONCLUSION = "cancelled"; + + 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.code).toBe(2); + expect(span.status.message).toBe("agent cancelled"); + }); + it("caps error messages at 5 entries", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" }); vi.stubGlobal("fetch", mockFetch); From 1608e4518878b0a25ad22141617268f54a8f4f58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 02:25:55 +0000 Subject: [PATCH 3/3] refactor: simplify otel cancelled status handling Agent-Logs-Url: https://github.com/github/gh-aw/sessions/4ed28949-5be5-4b74-867b-476f427741b5 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/send_otlp_span.cjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/send_otlp_span.cjs b/actions/setup/js/send_otlp_span.cjs index 9a65edeb2e4..2418b205336 100644 --- a/actions/setup/js/send_otlp_span.cjs +++ b/actions/setup/js/send_otlp_span.cjs @@ -703,9 +703,15 @@ async function sendJobConclusionSpan(spanName, options = {}) { // Mark the span as an error when the agent job failed, timed out, or was cancelled. const isAgentFailure = agentConclusion === "failure" || agentConclusion === "timed_out"; const isAgentCancelled = agentConclusion === "cancelled"; + const isAgentNonOK = isAgentFailure || isAgentCancelled; // STATUS_CODE_ERROR = 2, STATUS_CODE_OK = 1 - const statusCode = isAgentFailure || isAgentCancelled ? 2 : 1; - let statusMessage = isAgentFailure ? `agent ${agentConclusion}` : isAgentCancelled ? "agent cancelled" : undefined; + const statusCode = isAgentNonOK ? 2 : 1; + let statusMessage; + if (isAgentFailure) { + statusMessage = `agent ${agentConclusion}`; + } else if (isAgentCancelled) { + statusMessage = "agent cancelled"; + } // Always read agent_output.json so output metrics are available on all outcomes. const agentOutput = readJSONIfExists("/tmp/gh-aw/agent_output.json") || {};