diff --git a/actions/setup/js/otlp.cjs b/actions/setup/js/otlp.cjs index 446818fd585..2c74980d587 100644 --- a/actions/setup/js/otlp.cjs +++ b/actions/setup/js/otlp.cjs @@ -117,6 +117,7 @@ async function logSpan(toolName, attributes = {}, options = {}) { refName: process.env.GITHUB_REF_NAME || "", headRef: process.env.GITHUB_HEAD_REF || "", sha: process.env.GITHUB_SHA || "", + job: process.env.GITHUB_JOB || "", workflowRef: process.env.GH_AW_CURRENT_WORKFLOW_REF || process.env.GITHUB_WORKFLOW_REF || "", staged, }); diff --git a/actions/setup/js/otlp.test.cjs b/actions/setup/js/otlp.test.cjs index 96d2b1015ff..357384ec98b 100644 --- a/actions/setup/js/otlp.test.cjs +++ b/actions/setup/js/otlp.test.cjs @@ -101,6 +101,7 @@ describe("otlp.cjs", () => { GITHUB_REF_NAME: process.env.GITHUB_REF_NAME, GITHUB_HEAD_REF: process.env.GITHUB_HEAD_REF, GITHUB_SHA: process.env.GITHUB_SHA, + GITHUB_JOB: process.env.GITHUB_JOB, GITHUB_WORKFLOW_REF: process.env.GITHUB_WORKFLOW_REF, GH_AW_CURRENT_WORKFLOW_REF: process.env.GH_AW_CURRENT_WORKFLOW_REF, GH_AW_INFO_STAGED: process.env.GH_AW_INFO_STAGED, @@ -414,6 +415,14 @@ describe("otlp.cjs", () => { expect(mockBuildGitHubActionsResourceAttributes).toHaveBeenCalledWith(expect.objectContaining({ sha: "abc123def456" })); }); + + it("passes GITHUB_JOB to buildGitHubActionsResourceAttributes when set", async () => { + process.env.GITHUB_JOB = "agent"; + + await otlp.logSpan("my-scanner", {}); + + expect(mockBuildGitHubActionsResourceAttributes).toHaveBeenCalledWith(expect.objectContaining({ job: "agent" })); + }); }); // --------------------------------------------------------------------------- diff --git a/actions/setup/js/send_otlp_span.cjs b/actions/setup/js/send_otlp_span.cjs index 6db1766744c..1b102e37f98 100644 --- a/actions/setup/js/send_otlp_span.cjs +++ b/actions/setup/js/send_otlp_span.cjs @@ -297,13 +297,14 @@ function buildOTLPResourceAttributes(serviceName, scopeVersion, resourceAttribut * refName?: string, * headRef?: string, * sha?: string, + * job?: string, * workflowRef?: string, * staged: boolean, * runAttempt?: string, * }} ctx * @returns {Array<{key: string, value: object}>} */ -function buildGitHubActionsResourceAttributes({ repository, runId, eventName = "", ref = "", refName = "", headRef = "", sha = "", workflowRef = "", staged, runAttempt = "1" }) { +function buildGitHubActionsResourceAttributes({ repository, runId, eventName = "", ref = "", refName = "", headRef = "", sha = "", job = "", workflowRef = "", staged, runAttempt = "1" }) { const resourceAttributes = [buildAttr("github.repository", repository), buildAttr("github.run_id", runId), buildAttr("github.run_attempt", runAttempt)]; if (repository && runId && repository.includes("/")) { const [owner, repo] = repository.split("/"); @@ -324,6 +325,9 @@ function buildGitHubActionsResourceAttributes({ repository, runId, eventName = " if (sha) { resourceAttributes.push(buildAttr("github.sha", sha)); } + if (job) { + resourceAttributes.push(buildAttr("github.job", job)); + } if (workflowRef) { resourceAttributes.push(buildAttr("github.workflow_ref", workflowRef)); } @@ -873,6 +877,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 job = process.env.GITHUB_JOB || ""; const workflowRef = process.env.GH_AW_CURRENT_WORKFLOW_REF || process.env.GITHUB_WORKFLOW_REF || ""; const attributes = [ @@ -914,7 +919,7 @@ async function sendJobSetupSpan(options = {}) { attributes.push(...buildExperimentAttributes(experimentAssignments)); attributes.push(...buildEpisodeAttributesFromContext(awInfo, runId, runAttempt)); - const resourceAttributes = buildGitHubActionsResourceAttributes({ repository, runId, eventName, ref, refName, headRef, sha, workflowRef, staged, runAttempt }); + const resourceAttributes = buildGitHubActionsResourceAttributes({ repository, runId, eventName, ref, refName, headRef, sha, job, workflowRef, staged, runAttempt }); const payload = buildOTLPPayload({ traceId, @@ -1232,6 +1237,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 job = process.env.GITHUB_JOB || ""; const workflowRef = process.env.GITHUB_WORKFLOW_REF || ""; // Agent conclusion is passed to downstream jobs via GH_AW_AGENT_CONCLUSION. @@ -1365,7 +1371,7 @@ async function sendJobConclusionSpan(spanName, options = {}) { const conclusionExperimentAssignments = readExperimentAssignments(); attributes.push(...buildExperimentAttributes(conclusionExperimentAssignments)); - const resourceAttributes = buildGitHubActionsResourceAttributes({ repository, runId, eventName, ref, refName, headRef, sha, workflowRef, staged, runAttempt }); + const resourceAttributes = buildGitHubActionsResourceAttributes({ repository, runId, eventName, ref, refName, headRef, sha, job, workflowRef, staged, runAttempt }); // OpenTelemetry semantic convention for exceptions. Each event has // name="exception" with "exception.type" and "exception.message" attributes, // making individual errors queryable and classifiable in backends like diff --git a/actions/setup/js/send_otlp_span.test.cjs b/actions/setup/js/send_otlp_span.test.cjs index 4b806b3b734..d3b0619cc32 100644 --- a/actions/setup/js/send_otlp_span.test.cjs +++ b/actions/setup/js/send_otlp_span.test.cjs @@ -1074,6 +1074,7 @@ describe("sendJobSetupSpan", () => { "GITHUB_REF_NAME", "GITHUB_HEAD_REF", "GITHUB_SHA", + "GITHUB_JOB", "GITHUB_WORKFLOW_REF", "GH_AW_INFO_VERSION", "GH_AW_INFO_STAGED", @@ -1539,6 +1540,20 @@ describe("sendJobSetupSpan", () => { expect(resourceKeys).not.toContain("github.workflow_ref"); }); + it("includes github.job as resource attribute when GITHUB_JOB is set", async () => { + const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" }); + vi.stubGlobal("fetch", mockFetch); + + process.env.GH_AW_OTLP_ENDPOINTS = JSON.stringify([{ url: "https://traces.example.com" }]); + process.env.GITHUB_JOB = "agent"; + + await sendJobSetupSpan(); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "github.job", value: { stringValue: "agent" } }); + }); + 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); @@ -2106,6 +2121,7 @@ describe("sendJobConclusionSpan", () => { "GITHUB_REF_NAME", "GITHUB_HEAD_REF", "GITHUB_SHA", + "GITHUB_JOB", "GITHUB_WORKFLOW_REF", "INPUT_JOB_NAME", "GH_AW_AGENT_CONCLUSION", @@ -3999,6 +4015,20 @@ describe("sendJobConclusionSpan", () => { expect(resourceKeys).not.toContain("github.workflow_ref"); }); + it("includes github.job as resource attribute when GITHUB_JOB is set", async () => { + const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" }); + vi.stubGlobal("fetch", mockFetch); + + process.env.GH_AW_OTLP_ENDPOINTS = JSON.stringify([{ url: "https://traces.example.com" }]); + process.env.GITHUB_JOB = "conclusion"; + + 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.job", value: { stringValue: "conclusion" } }); + }); + describe("staged / deployment.environment", () => { let readFileSpy;