diff --git a/actions/setup/js/send_otlp_span.cjs b/actions/setup/js/send_otlp_span.cjs index e174f892a4e..bbcda664338 100644 --- a/actions/setup/js/send_otlp_span.cjs +++ b/actions/setup/js/send_otlp_span.cjs @@ -363,6 +363,7 @@ async function sendJobSetupSpan(options = {}) { const awInfo = readJSONIfExists("/tmp/gh-aw/aw_info.json") || {}; const rawContextTraceId = typeof awInfo.context?.otel_trace_id === "string" ? awInfo.context.otel_trace_id.trim().toLowerCase() : ""; const contextTraceId = isValidTraceId(rawContextTraceId) ? rawContextTraceId : ""; + const staged = awInfo.staged === true; const traceId = optionsTraceId || inputTraceId || contextTraceId || generateTraceId(); @@ -410,6 +411,7 @@ async function sendJobSetupSpan(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 14852f090a7..f473b228554 100644 --- a/actions/setup/js/send_otlp_span.test.cjs +++ b/actions/setup/js/send_otlp_span.test.cjs @@ -1012,6 +1012,73 @@ describe("sendJobSetupSpan", () => { const keys = span.attributes.map(a => a.key); expect(keys).not.toContain("gh-aw.engine.id"); }); + + 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 deployment.environment=production when aw_info.json is absent", 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; + expect(resourceAttrs).toContainEqual({ key: "deployment.environment", value: { stringValue: "production" } }); + }); + + it("sets 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 sendJobSetupSpan(); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "deployment.environment", value: { stringValue: "staging" } }); + }); + + it("sets 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 sendJobSetupSpan(); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "deployment.environment", value: { stringValue: "production" } }); + }); + }); }); // ---------------------------------------------------------------------------