From 848ecbbdb6a1faf500134388804ae38d930450e8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Apr 2026 08:58:47 +0000
Subject: [PATCH 1/3] Initial plan


From 812a2e339686d544d54abb7957ed737b32b715c1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Apr 2026 09:04:04 +0000
Subject: [PATCH 2/3] feat: add github.actions.run_url resource attribute to
 OTel spans

Adds github.actions.run_url to the resource attributes of both
sendJobSetupSpan and sendJobConclusionSpan. The URL uses
GITHUB_SERVER_URL (falling back to https://github.com) so it
correctly handles GitHub Enterprise Server installations. The
attribute is only added when both repository and runId are non-empty.

Tests added for: default URL, custom GITHUB_SERVER_URL (GHES), and
omission when fields are missing.

Closes #(issue)

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5833a1d6-639c-4678-a409-4bf34bf6e15f

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
 actions/setup/js/send_otlp_span.cjs      |   8 ++
 actions/setup/js/send_otlp_span.test.cjs | 108 +++++++++++++++++++++++
 2 files changed, 116 insertions(+)

diff --git a/actions/setup/js/send_otlp_span.cjs b/actions/setup/js/send_otlp_span.cjs
index 6083cebc234..c7b70311dd6 100644
--- a/actions/setup/js/send_otlp_span.cjs
+++ b/actions/setup/js/send_otlp_span.cjs
@@ -385,7 +385,11 @@ async function sendJobSetupSpan(options = {}) {
     attributes.push(buildAttr("gh-aw.engine.id", engineId));
   }
 
+  const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
   const resourceAttributes = [buildAttr("github.repository", repository), buildAttr("github.run_id", runId)];
+  if (repository && runId) {
+    resourceAttributes.push(buildAttr("github.actions.run_url", `${serverUrl}/${repository}/actions/runs/${runId}`));
+  }
   if (eventName) {
     resourceAttributes.push(buildAttr("github.event_name", eventName));
   }
@@ -547,7 +551,11 @@ async function sendJobConclusionSpan(spanName, options = {}) {
     attributes.push(buildAttr("gh-aw.error.messages", errorMessages.join(" | ")));
   }
 
+  const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
   const resourceAttributes = [buildAttr("github.repository", repository), buildAttr("github.run_id", runId)];
+  if (repository && runId) {
+    resourceAttributes.push(buildAttr("github.actions.run_url", `${serverUrl}/${repository}/actions/runs/${runId}`));
+  }
   if (eventName) {
     resourceAttributes.push(buildAttr("github.event_name", eventName));
   }
diff --git a/actions/setup/js/send_otlp_span.test.cjs b/actions/setup/js/send_otlp_span.test.cjs
index 528ed0e25f6..8d5edd6a315 100644
--- a/actions/setup/js/send_otlp_span.test.cjs
+++ b/actions/setup/js/send_otlp_span.test.cjs
@@ -818,6 +818,60 @@ describe("sendJobSetupSpan", () => {
     expect(resourceKeys).not.toContain("github.event_name");
   });
 
+  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);
+
+    process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://traces.example.com";
+    process.env.GITHUB_REPOSITORY = "owner/repo";
+    process.env.GITHUB_RUN_ID = "987654321";
+    delete process.env.GITHUB_SERVER_URL;
+
+    await sendJobSetupSpan();
+
+    const body = JSON.parse(mockFetch.mock.calls[0][1].body);
+    const resourceAttrs = body.resourceSpans[0].resource.attributes;
+    expect(resourceAttrs).toContainEqual({
+      key: "github.actions.run_url",
+      value: { stringValue: "https://github.com/owner/repo/actions/runs/987654321" },
+    });
+  });
+
+  it("uses GITHUB_SERVER_URL for github.actions.run_url in sendJobSetupSpan", 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";
+    process.env.GITHUB_SERVER_URL = "https://github.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: "github.actions.run_url",
+      value: { stringValue: "https://github.example.com/owner/repo/actions/runs/987654321" },
+    });
+  });
+
+  it("omits github.actions.run_url when repository or run_id is missing in sendJobSetupSpan", 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";
+    delete process.env.GITHUB_REPOSITORY;
+    delete process.env.GITHUB_RUN_ID;
+
+    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.actions.run_url");
+  });
+
   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);
@@ -1090,6 +1144,60 @@ describe("sendJobConclusionSpan", () => {
     expect(resourceKeys).not.toContain("github.event_name");
   });
 
+  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);
+
+    process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://traces.example.com";
+    process.env.GITHUB_REPOSITORY = "owner/repo";
+    process.env.GITHUB_RUN_ID = "987654321";
+    delete process.env.GITHUB_SERVER_URL;
+
+    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.actions.run_url",
+      value: { stringValue: "https://github.com/owner/repo/actions/runs/987654321" },
+    });
+  });
+
+  it("uses GITHUB_SERVER_URL for github.actions.run_url in sendJobConclusionSpan", 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";
+    process.env.GITHUB_SERVER_URL = "https://github.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;
+    expect(resourceAttrs).toContainEqual({
+      key: "github.actions.run_url",
+      value: { stringValue: "https://github.example.com/owner/repo/actions/runs/987654321" },
+    });
+  });
+
+  it("omits github.actions.run_url when repository or run_id is missing in sendJobConclusionSpan", 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";
+    delete process.env.GITHUB_REPOSITORY;
+    delete process.env.GITHUB_RUN_ID;
+
+    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.actions.run_url");
+  });
+
   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);

From edca181bdecda2483fe150d9ccdab0a4170c7c39 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Apr 2026 09:10:10 +0000
Subject: [PATCH 3/3] refactor: use buildWorkflowRunUrl helper to construct
 github.actions.run_url

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6df4e570-51df-4bdd-8a68-a816acd91dc8

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
 actions/setup/js/send_otlp_span.cjs | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/actions/setup/js/send_otlp_span.cjs b/actions/setup/js/send_otlp_span.cjs
index c7b70311dd6..c3739f48200 100644
--- a/actions/setup/js/send_otlp_span.cjs
+++ b/actions/setup/js/send_otlp_span.cjs
@@ -4,6 +4,7 @@
 const { randomBytes } = require("crypto");
 const fs = require("fs");
 const { nowMs } = require("./performance_now.cjs");
+const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
 
 /**
  * send_otlp_span.cjs
@@ -385,10 +386,10 @@ async function sendJobSetupSpan(options = {}) {
     attributes.push(buildAttr("gh-aw.engine.id", engineId));
   }
 
-  const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
   const resourceAttributes = [buildAttr("github.repository", repository), buildAttr("github.run_id", runId)];
   if (repository && runId) {
-    resourceAttributes.push(buildAttr("github.actions.run_url", `${serverUrl}/${repository}/actions/runs/${runId}`));
+    const [owner, repo] = repository.split("/");
+    resourceAttributes.push(buildAttr("github.actions.run_url", buildWorkflowRunUrl({ runId }, { owner, repo })));
   }
   if (eventName) {
     resourceAttributes.push(buildAttr("github.event_name", eventName));
@@ -551,10 +552,10 @@ async function sendJobConclusionSpan(spanName, options = {}) {
     attributes.push(buildAttr("gh-aw.error.messages", errorMessages.join(" | ")));
   }
 
-  const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
   const resourceAttributes = [buildAttr("github.repository", repository), buildAttr("github.run_id", runId)];
   if (repository && runId) {
-    resourceAttributes.push(buildAttr("github.actions.run_url", `${serverUrl}/${repository}/actions/runs/${runId}`));
+    const [owner, repo] = repository.split("/");
+    resourceAttributes.push(buildAttr("github.actions.run_url", buildWorkflowRunUrl({ runId }, { owner, repo })));
   }
   if (eventName) {
     resourceAttributes.push(buildAttr("github.event_name", eventName));
