From 9a50d2c5bd452549bdcc411150effd80dbf1ca41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:31:29 +0000 Subject: [PATCH 1/2] Initial plan From 788a07780e8569407a01a07c14c311479896ec91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:19:39 +0000 Subject: [PATCH 2/2] feat: add github.ref and github.sha to OTel span resource attributes Agent-Logs-Url: https://github.com/github/gh-aw/sessions/bd805b69-0bba-432a-8cdd-c765db803deb Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/send_otlp_span.cjs | 16 ++++ actions/setup/js/send_otlp_span.test.cjs | 116 +++++++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/actions/setup/js/send_otlp_span.cjs b/actions/setup/js/send_otlp_span.cjs index bbcda664338..eb5c218961f 100644 --- a/actions/setup/js/send_otlp_span.cjs +++ b/actions/setup/js/send_otlp_span.cjs @@ -389,6 +389,8 @@ async function sendJobSetupSpan(options = {}) { const actor = process.env.GITHUB_ACTOR || ""; const repository = process.env.GITHUB_REPOSITORY || ""; const eventName = process.env.GITHUB_EVENT_NAME || ""; + const ref = process.env.GITHUB_REF || ""; + const sha = process.env.GITHUB_SHA || ""; const attributes = [ buildAttr("gh-aw.job.name", jobName), @@ -411,6 +413,12 @@ async function sendJobSetupSpan(options = {}) { if (eventName) { resourceAttributes.push(buildAttr("github.event_name", eventName)); } + if (ref) { + resourceAttributes.push(buildAttr("github.ref", ref)); + } + if (sha) { + resourceAttributes.push(buildAttr("github.sha", sha)); + } resourceAttributes.push(buildAttr("deployment.environment", staged ? "staging" : "production")); const payload = buildOTLPPayload({ @@ -568,6 +576,8 @@ async function sendJobConclusionSpan(spanName, options = {}) { const actor = process.env.GITHUB_ACTOR || ""; const repository = process.env.GITHUB_REPOSITORY || ""; const eventName = process.env.GITHUB_EVENT_NAME || ""; + const ref = process.env.GITHUB_REF || ""; + const sha = process.env.GITHUB_SHA || ""; // Agent conclusion is passed to downstream jobs via GH_AW_AGENT_CONCLUSION. // Values: "success", "failure", "timed_out", "cancelled", "skipped". @@ -637,6 +647,12 @@ async function sendJobConclusionSpan(spanName, options = {}) { if (eventName) { resourceAttributes.push(buildAttr("github.event_name", eventName)); } + if (ref) { + resourceAttributes.push(buildAttr("github.ref", ref)); + } + if (sha) { + resourceAttributes.push(buildAttr("github.sha", sha)); + } resourceAttributes.push(buildAttr("deployment.environment", staged ? "staging" : "production")); const payload = buildOTLPPayload({ diff --git a/actions/setup/js/send_otlp_span.test.cjs b/actions/setup/js/send_otlp_span.test.cjs index f473b228554..dd105f4d16b 100644 --- a/actions/setup/js/send_otlp_span.test.cjs +++ b/actions/setup/js/send_otlp_span.test.cjs @@ -691,6 +691,8 @@ describe("sendJobSetupSpan", () => { "GITHUB_ACTOR", "GITHUB_REPOSITORY", "GITHUB_EVENT_NAME", + "GITHUB_REF", + "GITHUB_SHA", "GH_AW_INFO_VERSION", ]; let mkdirSpy, appendSpy; @@ -931,6 +933,62 @@ describe("sendJobSetupSpan", () => { expect(resourceKeys).not.toContain("github.event_name"); }); + it("includes github.ref as resource attribute when GITHUB_REF is 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"; + process.env.GITHUB_REF = "refs/heads/main"; + + await sendJobSetupSpan(); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "github.ref", value: { stringValue: "refs/heads/main" } }); + }); + + it("omits github.ref resource attribute when GITHUB_REF 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 sendJobSetupSpan(); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + const resourceKeys = resourceAttrs.map(a => a.key); + expect(resourceKeys).not.toContain("github.ref"); + }); + + it("includes github.sha as resource attribute when GITHUB_SHA is 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"; + process.env.GITHUB_SHA = "abc1234567890def"; + + await sendJobSetupSpan(); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "github.sha", value: { stringValue: "abc1234567890def" } }); + }); + + it("omits github.sha resource attribute when GITHUB_SHA 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 sendJobSetupSpan(); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + const resourceKeys = resourceAttrs.map(a => a.key); + expect(resourceKeys).not.toContain("github.sha"); + }); + it("includes github.actions.run_url as resource attribute when repository and run_id are set", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" }); vi.stubGlobal("fetch", mockFetch); @@ -1100,6 +1158,8 @@ describe("sendJobConclusionSpan", () => { "GITHUB_ACTOR", "GITHUB_REPOSITORY", "GITHUB_EVENT_NAME", + "GITHUB_REF", + "GITHUB_SHA", "INPUT_JOB_NAME", "GH_AW_AGENT_CONCLUSION", ]; @@ -1324,6 +1384,62 @@ describe("sendJobConclusionSpan", () => { expect(resourceKeys).not.toContain("github.event_name"); }); + it("includes github.ref as resource attribute when GITHUB_REF is 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"; + process.env.GITHUB_REF = "refs/heads/main"; + + await sendJobConclusionSpan("gh-aw.job.conclusion"); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "github.ref", value: { stringValue: "refs/heads/main" } }); + }); + + it("omits github.ref resource attribute when GITHUB_REF 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 resourceAttrs = body.resourceSpans[0].resource.attributes; + const resourceKeys = resourceAttrs.map(a => a.key); + expect(resourceKeys).not.toContain("github.ref"); + }); + + it("includes github.sha as resource attribute when GITHUB_SHA is 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"; + process.env.GITHUB_SHA = "abc1234567890def"; + + await sendJobConclusionSpan("gh-aw.job.conclusion"); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "github.sha", value: { stringValue: "abc1234567890def" } }); + }); + + it("omits github.sha resource attribute when GITHUB_SHA 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 resourceAttrs = body.resourceSpans[0].resource.attributes; + const resourceKeys = resourceAttrs.map(a => a.key); + expect(resourceKeys).not.toContain("github.sha"); + }); + it("includes github.actions.run_url as resource attribute when repository and run_id are set", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" }); vi.stubGlobal("fetch", mockFetch);