From 50d34ac335dd43c8dc7ea4242c8c88dd828d4b62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:27:16 +0000 Subject: [PATCH 1/2] Initial plan From 4ea04fea3e0edab0a052b72524891cb742f835a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:33:58 +0000 Subject: [PATCH 2/2] feat: add gh-aw.staged and deployment.environment to conclusion spans Reads awInfo.staged in sendJobConclusionSpan and propagates it as: - gh-aw.staged (boolean) in span attributes - deployment.environment ("staging"/"production") in resource attributes Adds tests for both staged=true and staged=false cases. Fixes # Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f78bba75-eeb5-4ffa-abe4-0c55ae22c8f6 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/send_otlp_span.cjs | 3 + actions/setup/js/send_otlp_span.test.cjs | 82 ++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/actions/setup/js/send_otlp_span.cjs b/actions/setup/js/send_otlp_span.cjs index db20badf015..e174f892a4e 100644 --- a/actions/setup/js/send_otlp_span.cjs +++ b/actions/setup/js/send_otlp_span.cjs @@ -559,6 +559,7 @@ async function sendJobConclusionSpan(spanName, options = {}) { const workflowName = awInfo.workflow_name || ""; const engineId = awInfo.engine_id || ""; const model = awInfo.model || ""; + const staged = awInfo.staged === true; const jobName = process.env.INPUT_JOB_NAME || ""; const runId = process.env.GITHUB_RUN_ID || ""; const runAttempt = awInfo.run_attempt || process.env.GITHUB_RUN_ATTEMPT || "1"; @@ -594,6 +595,7 @@ async function sendJobConclusionSpan(spanName, options = {}) { if (jobName) attributes.push(buildAttr("gh-aw.job.name", jobName)); if (engineId) attributes.push(buildAttr("gh-aw.engine.id", engineId)); if (model) attributes.push(buildAttr("gh-aw.model", model)); + attributes.push(buildAttr("gh-aw.staged", staged)); if (!isNaN(effectiveTokens) && effectiveTokens > 0) { attributes.push(buildAttr("gh-aw.effective_tokens", effectiveTokens)); } @@ -633,6 +635,7 @@ async function sendJobConclusionSpan(spanName, options = {}) { if (eventName) { resourceAttributes.push(buildAttr("github.event_name", eventName)); } + resourceAttributes.push(buildAttr("deployment.environment", staged ? "staging" : "production")); const payload = buildOTLPPayload({ traceId, diff --git a/actions/setup/js/send_otlp_span.test.cjs b/actions/setup/js/send_otlp_span.test.cjs index 45a5c26216a..14852f090a7 100644 --- a/actions/setup/js/send_otlp_span.test.cjs +++ b/actions/setup/js/send_otlp_span.test.cjs @@ -1616,4 +1616,86 @@ describe("sendJobConclusionSpan", () => { expect(keys).not.toContain("gh-aw.github.rate_limit.remaining"); }); }); + + describe("staged / deployment.environment", () => { + let readFileSpy; + + beforeEach(() => { + readFileSpy = vi.spyOn(fs, "readFileSync").mockImplementation(() => { + throw Object.assign(new Error("ENOENT"), { code: "ENOENT" }); + }); + }); + + afterEach(() => { + readFileSpy.mockRestore(); + }); + + it("sets gh-aw.staged=false and deployment.environment=production when staged is not set", 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"; + + 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 stagedAttr = span.attributes.find(a => a.key === "gh-aw.staged"); + expect(stagedAttr).toBeDefined(); + expect(stagedAttr.value.boolValue).toBe(false); + + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "deployment.environment", value: { stringValue: "production" } }); + }); + + it("sets gh-aw.staged=true and deployment.environment=staging when awInfo.staged=true", 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"; + + readFileSpy.mockImplementation(filePath => { + if (filePath === "/tmp/gh-aw/aw_info.json") { + return JSON.stringify({ staged: true }); + } + 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 stagedAttr = span.attributes.find(a => a.key === "gh-aw.staged"); + expect(stagedAttr).toBeDefined(); + expect(stagedAttr.value.boolValue).toBe(true); + + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "deployment.environment", value: { stringValue: "staging" } }); + }); + + it("sets gh-aw.staged=false and deployment.environment=production when awInfo.staged=false", 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"; + + readFileSpy.mockImplementation(filePath => { + if (filePath === "/tmp/gh-aw/aw_info.json") { + return JSON.stringify({ staged: false }); + } + 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 stagedAttr = span.attributes.find(a => a.key === "gh-aw.staged"); + expect(stagedAttr).toBeDefined(); + expect(stagedAttr.value.boolValue).toBe(false); + + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "deployment.environment", value: { stringValue: "production" } }); + }); + }); });