From 6fcef3e2177628448d681d3edfc2ce1beaa585f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 04:02:38 +0000 Subject: [PATCH 1/2] Initial plan From 2a89b86294b37e10cde3790da2c25fdac54e957a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 04:20:29 +0000 Subject: [PATCH 2/2] fix: enrich OTLP resource attributes with service.version, github.repository, github.run_id, github.event_name - Extend OTLPSpanOptions typedef with resourceAttributes optional field - buildOTLPPayload promotes scopeVersion to service.version resource attr - buildOTLPPayload merges caller-supplied resourceAttributes into resource block - sendJobSetupSpan reads GITHUB_EVENT_NAME and passes github.repository, github.run_id, github.event_name as resource attributes - sendJobConclusionSpan applies same resource attribute additions - Update tests to assert new resource attributes Fixes #[issue] Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b798b274-af7d-4ae5-8566-b2711bfa8a0f Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/action_otlp.test.cjs | 33 +++++ actions/setup/js/send_otlp_span.cjs | 24 ++- actions/setup/js/send_otlp_span.test.cjs | 179 ++++++++++++++++++++++- 3 files changed, 233 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/action_otlp.test.cjs b/actions/setup/js/action_otlp.test.cjs index 5132d30ef76..0973155a94e 100644 --- a/actions/setup/js/action_otlp.test.cjs +++ b/actions/setup/js/action_otlp.test.cjs @@ -130,6 +130,39 @@ describe("action_setup_otlp run()", () => { await expect(runSetup()).resolves.toBeUndefined(); fetchSpy.mockRestore(); }); + + it("includes github.repository, github.run_id resource attributes in setup span", async () => { + const tmpOut = path.join(path.dirname(__dirname), `action_setup_otlp_test_resource_attrs_${Date.now()}.txt`); + try { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:14317"; + process.env.GITHUB_REPOSITORY = "owner/repo"; + process.env.GITHUB_RUN_ID = "111222333"; + process.env.GITHUB_EVENT_NAME = "workflow_dispatch"; + process.env.GITHUB_OUTPUT = tmpOut; + process.env.GITHUB_ENV = tmpOut; + + let capturedBody; + const fetchSpy = vi.spyOn(global, "fetch").mockImplementation((_url, opts) => { + capturedBody = opts?.body; + return Promise.resolve(new Response(null, { status: 200 })); + }); + + await runSetup(); + + const payload = JSON.parse(capturedBody); + const resourceAttrs = payload?.resourceSpans?.[0]?.resource?.attributes ?? []; + expect(resourceAttrs).toContainEqual({ key: "github.repository", value: { stringValue: "owner/repo" } }); + expect(resourceAttrs).toContainEqual({ key: "github.run_id", value: { stringValue: "111222333" } }); + expect(resourceAttrs).toContainEqual({ key: "github.event_name", value: { stringValue: "workflow_dispatch" } }); + + fetchSpy.mockRestore(); + } finally { + fs.rmSync(tmpOut, { force: true }); + delete process.env.GITHUB_REPOSITORY; + delete process.env.GITHUB_RUN_ID; + delete process.env.GITHUB_EVENT_NAME; + } + }); }); // --------------------------------------------------------------------------- diff --git a/actions/setup/js/send_otlp_span.cjs b/actions/setup/js/send_otlp_span.cjs index d478d2a3681..1bb8212d4be 100644 --- a/actions/setup/js/send_otlp_span.cjs +++ b/actions/setup/js/send_otlp_span.cjs @@ -83,6 +83,7 @@ function buildAttr(key, value) { * @property {string} serviceName - Value for the service.name resource attribute * @property {string} [scopeVersion] - gh-aw version string (e.g. from GH_AW_INFO_VERSION) * @property {Array<{key: string, value: object}>} attributes - Span attributes + * @property {Array<{key: string, value: object}>} [resourceAttributes] - Extra resource attributes (e.g. github.repository, github.run_id) * @property {number} [statusCode] - OTLP status code: 0=UNSET, 1=OK, 2=ERROR (defaults to 1) * @property {string} [statusMessage] - Human-readable status message (included when statusCode is 2) */ @@ -93,18 +94,23 @@ function buildAttr(key, value) { * @param {OTLPSpanOptions} opts * @returns {object} - Ready to be serialised as JSON and POSTed to `/v1/traces` */ -function buildOTLPPayload({ traceId, spanId, parentSpanId, spanName, startMs, endMs, serviceName, scopeVersion, attributes, statusCode, statusMessage }) { +function buildOTLPPayload({ traceId, spanId, parentSpanId, spanName, startMs, endMs, serviceName, scopeVersion, attributes, resourceAttributes, statusCode, statusMessage }) { const code = typeof statusCode === "number" ? statusCode : 1; // STATUS_CODE_OK /** @type {{ code: number, message?: string }} */ const status = { code }; if (statusMessage) { status.message = statusMessage; } + const baseResourceAttrs = [buildAttr("service.name", serviceName)]; + if (scopeVersion && scopeVersion !== "unknown") { + baseResourceAttrs.push(buildAttr("service.version", scopeVersion)); + } + const allResourceAttrs = resourceAttributes ? [...baseResourceAttrs, ...resourceAttributes] : baseResourceAttrs; return { resourceSpans: [ { resource: { - attributes: [buildAttr("service.name", serviceName)], + attributes: allResourceAttrs, }, scopeSpans: [ { @@ -362,6 +368,7 @@ async function sendJobSetupSpan(options = {}) { const runId = process.env.GITHUB_RUN_ID || ""; const actor = process.env.GITHUB_ACTOR || ""; const repository = process.env.GITHUB_REPOSITORY || ""; + const eventName = process.env.GITHUB_EVENT_NAME || ""; const attributes = [buildAttr("gh-aw.job.name", jobName), buildAttr("gh-aw.workflow.name", workflowName), buildAttr("gh-aw.run.id", runId), buildAttr("gh-aw.run.actor", actor), buildAttr("gh-aw.repository", repository)]; @@ -369,6 +376,11 @@ async function sendJobSetupSpan(options = {}) { attributes.push(buildAttr("gh-aw.engine.id", engineId)); } + const resourceAttributes = [buildAttr("github.repository", repository), buildAttr("github.run_id", runId)]; + if (eventName) { + resourceAttributes.push(buildAttr("github.event_name", eventName)); + } + const payload = buildOTLPPayload({ traceId, spanId, @@ -378,6 +390,7 @@ async function sendJobSetupSpan(options = {}) { serviceName, scopeVersion: process.env.GH_AW_INFO_VERSION || "unknown", attributes, + resourceAttributes, }); await sendOTLPSpan(endpoint, payload); @@ -483,6 +496,7 @@ async function sendJobConclusionSpan(spanName, options = {}) { const runId = process.env.GITHUB_RUN_ID || ""; const actor = process.env.GITHUB_ACTOR || ""; const repository = process.env.GITHUB_REPOSITORY || ""; + const eventName = process.env.GITHUB_EVENT_NAME || ""; // Agent conclusion is passed to downstream jobs via GH_AW_AGENT_CONCLUSION. // Values: "success", "failure", "timed_out", "cancelled", "skipped". @@ -506,6 +520,11 @@ async function sendJobConclusionSpan(spanName, options = {}) { attributes.push(buildAttr("gh-aw.agent.conclusion", agentConclusion)); } + const resourceAttributes = [buildAttr("github.repository", repository), buildAttr("github.run_id", runId)]; + if (eventName) { + resourceAttributes.push(buildAttr("github.event_name", eventName)); + } + const payload = buildOTLPPayload({ traceId, spanId: generateSpanId(), @@ -516,6 +535,7 @@ async function sendJobConclusionSpan(spanName, options = {}) { serviceName, scopeVersion: version, attributes, + resourceAttributes, statusCode, statusMessage, }); diff --git a/actions/setup/js/send_otlp_span.test.cjs b/actions/setup/js/send_otlp_span.test.cjs index 6638c7579ff..7fd25eb0c1c 100644 --- a/actions/setup/js/send_otlp_span.test.cjs +++ b/actions/setup/js/send_otlp_span.test.cjs @@ -167,6 +167,7 @@ describe("buildOTLPPayload", () => { // Resource expect(rs.resource.attributes).toContainEqual({ key: "service.name", value: { stringValue: "gh-aw" } }); + expect(rs.resource.attributes).toContainEqual({ key: "service.version", value: { stringValue: "v1.2.3" } }); // Scope — name is always "gh-aw"; version comes from scopeVersion expect(rs.scopeSpans).toHaveLength(1); @@ -198,6 +199,53 @@ describe("buildOTLPPayload", () => { expect(payload.resourceSpans[0].scopeSpans[0].scope.version).toBe("unknown"); }); + it("omits service.version from resource attributes when scopeVersion is 'unknown'", () => { + const payload = buildOTLPPayload({ + traceId: "a".repeat(32), + spanId: "b".repeat(16), + spanName: "test", + startMs: 0, + endMs: 1, + serviceName: "gh-aw", + scopeVersion: "unknown", + attributes: [], + }); + const resourceKeys = payload.resourceSpans[0].resource.attributes.map(a => a.key); + expect(resourceKeys).not.toContain("service.version"); + }); + + it("omits service.version from resource attributes when scopeVersion is omitted", () => { + const payload = buildOTLPPayload({ + traceId: "a".repeat(32), + spanId: "b".repeat(16), + spanName: "test", + startMs: 0, + endMs: 1, + serviceName: "gh-aw", + attributes: [], + }); + const resourceKeys = payload.resourceSpans[0].resource.attributes.map(a => a.key); + expect(resourceKeys).not.toContain("service.version"); + }); + + it("merges caller-supplied resourceAttributes into the resource block", () => { + const payload = buildOTLPPayload({ + traceId: "a".repeat(32), + spanId: "b".repeat(16), + spanName: "test", + startMs: 0, + endMs: 1, + serviceName: "gh-aw", + scopeVersion: "v1.0.0", + attributes: [], + resourceAttributes: [buildAttr("github.repository", "owner/repo"), buildAttr("github.run_id", "123")], + }); + const rs = payload.resourceSpans[0]; + expect(rs.resource.attributes).toContainEqual({ key: "github.repository", value: { stringValue: "owner/repo" } }); + expect(rs.resource.attributes).toContainEqual({ key: "github.run_id", value: { stringValue: "123" } }); + expect(rs.resource.attributes).toContainEqual({ key: "service.version", value: { stringValue: "v1.0.0" } }); + }); + it("includes parentSpanId in span when provided", () => { const payload = buildOTLPPayload({ traceId: "a".repeat(32), @@ -518,7 +566,19 @@ describe("sendOTLPSpan with OTEL_EXPORTER_OTLP_HEADERS", () => { describe("sendJobSetupSpan", () => { /** @type {Record} */ const savedEnv = {}; - const envKeys = ["OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_SERVICE_NAME", "INPUT_JOB_NAME", "INPUT_TRACE_ID", "GH_AW_INFO_WORKFLOW_NAME", "GH_AW_INFO_ENGINE_ID", "GITHUB_RUN_ID", "GITHUB_ACTOR", "GITHUB_REPOSITORY"]; + const envKeys = [ + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_SERVICE_NAME", + "INPUT_JOB_NAME", + "INPUT_TRACE_ID", + "GH_AW_INFO_WORKFLOW_NAME", + "GH_AW_INFO_ENGINE_ID", + "GITHUB_RUN_ID", + "GITHUB_ACTOR", + "GITHUB_REPOSITORY", + "GITHUB_EVENT_NAME", + "GH_AW_INFO_VERSION", + ]; let mkdirSpy, appendSpy; beforeEach(() => { @@ -697,6 +757,64 @@ describe("sendJobSetupSpan", () => { expect(resourceAttrs).toContainEqual({ key: "service.name", value: { stringValue: "my-service" } }); }); + it("includes github.repository and github.run_id as resource attributes", 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_REPOSITORY = "owner/repo"; + process.env.GITHUB_RUN_ID = "987654321"; + + await sendJobSetupSpan(); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "github.repository", value: { stringValue: "owner/repo" } }); + expect(resourceAttrs).toContainEqual({ key: "github.run_id", value: { stringValue: "987654321" } }); + }); + + it("includes github.event_name as resource attribute when GITHUB_EVENT_NAME 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_EVENT_NAME = "workflow_dispatch"; + + await sendJobSetupSpan(); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "github.event_name", value: { stringValue: "workflow_dispatch" } }); + }); + + it("omits github.event_name resource attribute when GITHUB_EVENT_NAME 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.event_name"); + }); + + it("includes service.version resource attribute when GH_AW_INFO_VERSION 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.GH_AW_INFO_VERSION = "v1.2.3"; + + await sendJobSetupSpan(); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + const resourceAttrs = body.resourceSpans[0].resource.attributes; + expect(resourceAttrs).toContainEqual({ key: "service.version", value: { stringValue: "v1.2.3" } }); + }); + it("omits gh-aw.engine.id attribute when engine is not set", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" }); vi.stubGlobal("fetch", mockFetch); @@ -729,6 +847,7 @@ describe("sendJobConclusionSpan", () => { "GITHUB_RUN_ID", "GITHUB_ACTOR", "GITHUB_REPOSITORY", + "GITHUB_EVENT_NAME", "INPUT_JOB_NAME", ]; let mkdirSpy, appendSpy; @@ -878,4 +997,62 @@ describe("sendJobConclusionSpan", () => { const span = body.resourceSpans[0].scopeSpans[0].spans[0]; expect(span.traceId).toBe("f".repeat(32)); }); + + it("includes github.repository and github.run_id as resource attributes", 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_REPOSITORY = "owner/repo"; + process.env.GITHUB_RUN_ID = "987654321"; + + 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.repository", value: { stringValue: "owner/repo" } }); + expect(resourceAttrs).toContainEqual({ key: "github.run_id", value: { stringValue: "987654321" } }); + }); + + it("includes github.event_name as resource attribute when GITHUB_EVENT_NAME 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_EVENT_NAME = "pull_request"; + + 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.event_name", value: { stringValue: "pull_request" } }); + }); + + it("omits github.event_name resource attribute when GITHUB_EVENT_NAME 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.event_name"); + }); + + it("includes service.version resource attribute when version is known", 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.GH_AW_INFO_VERSION = "v3.0.0"; + + 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: "service.version", value: { stringValue: "v3.0.0" } }); + }); });