diff --git a/actions/setup/js/send_otlp_span.cjs b/actions/setup/js/send_otlp_span.cjs index 29fb64a739f..fa5322c4e7e 100644 --- a/actions/setup/js/send_otlp_span.cjs +++ b/actions/setup/js/send_otlp_span.cjs @@ -495,6 +495,7 @@ async function sendJobSetupSpan(options = {}) { const refName = process.env.GITHUB_REF_NAME || ""; const headRef = process.env.GITHUB_HEAD_REF || ""; const sha = process.env.GITHUB_SHA || ""; + const workflowRef = process.env.GITHUB_WORKFLOW_REF || ""; const attributes = [ buildAttr("gh-aw.job.name", jobName), @@ -533,6 +534,9 @@ async function sendJobSetupSpan(options = {}) { if (sha) { resourceAttributes.push(buildAttr("github.sha", sha)); } + if (workflowRef) { + resourceAttributes.push(buildAttr("github.workflow_ref", workflowRef)); + } resourceAttributes.push(buildAttr("deployment.environment", staged ? "staging" : "production")); const payload = buildOTLPPayload({ @@ -705,6 +709,7 @@ async function sendJobConclusionSpan(spanName, options = {}) { const refName = process.env.GITHUB_REF_NAME || ""; const headRef = process.env.GITHUB_HEAD_REF || ""; const sha = process.env.GITHUB_SHA || ""; + const workflowRef = process.env.GITHUB_WORKFLOW_REF || ""; // Agent conclusion is passed to downstream jobs via GH_AW_AGENT_CONCLUSION. // Values: "success", "failure", "timed_out", "cancelled", "skipped". @@ -819,6 +824,9 @@ async function sendJobConclusionSpan(spanName, options = {}) { if (sha) { resourceAttributes.push(buildAttr("github.sha", sha)); } + if (workflowRef) { + resourceAttributes.push(buildAttr("github.workflow_ref", workflowRef)); + } resourceAttributes.push(buildAttr("deployment.environment", staged ? "staging" : "production")); // Build OTel exception span events — one per error — following the diff --git a/actions/setup/js/send_otlp_span.test.cjs b/actions/setup/js/send_otlp_span.test.cjs index 50cd0325983..fb86464e261 100644 --- a/actions/setup/js/send_otlp_span.test.cjs +++ b/actions/setup/js/send_otlp_span.test.cjs @@ -921,6 +921,7 @@ describe("sendJobSetupSpan", () => { "GITHUB_REF_NAME", "GITHUB_HEAD_REF", "GITHUB_SHA", + "GITHUB_WORKFLOW_REF", "GH_AW_INFO_VERSION", "GH_AW_INFO_STAGED", ]; @@ -1286,6 +1287,34 @@ describe("sendJobSetupSpan", () => { expect(resourceKeys).not.toContain("github.sha"); }); + it("includes github.workflow_ref as resource attribute when GITHUB_WORKFLOW_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_WORKFLOW_REF = "owner/repo/.github/workflows/my-workflow.yml@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.workflow_ref", value: { stringValue: "owner/repo/.github/workflows/my-workflow.yml@refs/heads/main" } }); + }); + + it("omits github.workflow_ref resource attribute when GITHUB_WORKFLOW_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.workflow_ref"); + }); + 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); @@ -1554,6 +1583,7 @@ describe("sendJobConclusionSpan", () => { "GITHUB_REF_NAME", "GITHUB_HEAD_REF", "GITHUB_SHA", + "GITHUB_WORKFLOW_REF", "INPUT_JOB_NAME", "GH_AW_AGENT_CONCLUSION", "GH_AW_INFO_WORKFLOW_NAME", @@ -2914,6 +2944,34 @@ describe("sendJobConclusionSpan", () => { }); }); + it("includes github.workflow_ref as resource attribute when GITHUB_WORKFLOW_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_WORKFLOW_REF = "owner/repo/.github/workflows/my-workflow.yml@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.workflow_ref", value: { stringValue: "owner/repo/.github/workflows/my-workflow.yml@refs/heads/main" } }); + }); + + it("omits github.workflow_ref resource attribute when GITHUB_WORKFLOW_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.workflow_ref"); + }); + describe("staged / deployment.environment", () => { let readFileSpy;