feat(otel): add github.actions.run_url resource attribute to all spans#24691
feat(otel): add github.actions.run_url resource attribute to all spans#24691
github.actions.run_url resource attribute to all spans#24691Conversation
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>
github.actions.run_url resource attribute to all spans
actions/setup/js/send_otlp_span.cjs
Outdated
| 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}`)); |
There was a problem hiding this comment.
@copilot use existing helper function to create run url
There was a problem hiding this comment.
Done in edca181. Now imports and uses buildWorkflowRunUrl from workflow_metadata_helpers.cjs in both span functions, splitting repository (owner/repo) into { owner, repo } as expected by the helper.
…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>
|
Hey This PR is well-structured and aligns with the project's contribution process. Here's a quick summary of the check results:
Verdict: 🟢 Aligned — this PR looks ready for maintainer review once CI checks pass and the draft status is removed.
|
There was a problem hiding this comment.
Pull request overview
Adds a precomputed GitHub Actions workflow run URL (github.actions.run_url) as an OTEL resource attribute for job setup and conclusion spans, including support for GitHub Enterprise Server via GITHUB_SERVER_URL.
Changes:
- Emit
github.actions.run_urlinsendJobSetupSpanandsendJobConclusionSpanwhenGITHUB_REPOSITORYandGITHUB_RUN_IDare present. - Reuse
buildWorkflowRunUrlhelper for run URL construction. - Add unit tests covering default
github.com, GHES (GITHUB_SERVER_URL), and omission when inputs are missing.
Show a summary per file
| File | Description |
|---|---|
| actions/setup/js/send_otlp_span.cjs | Adds github.actions.run_url resource attribute generation for setup/conclusion spans. |
| actions/setup/js/send_otlp_span.test.cjs | Adds tests validating run URL emission and GHES/default behavior. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
actions/setup/js/send_otlp_span.test.cjs:1175
- Same env-leak issue as in the setup-span tests: this suite mutates
process.env.GITHUB_SERVER_URLbut thesendJobConclusionSpanenv save/restore list doesn’t include it, which can make tests order-dependent. IncludeGITHUB_SERVER_URLinenvKeys(or save/restore it within these tests).
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");
- Files reviewed: 2/2 changed files
- Comments generated: 2
| const resourceAttributes = [buildAttr("github.repository", repository), buildAttr("github.run_id", runId)]; | ||
| if (repository && runId) { | ||
| const [owner, repo] = repository.split("/"); | ||
| resourceAttributes.push(buildAttr("github.actions.run_url", buildWorkflowRunUrl({ runId }, { owner, repo }))); | ||
| } |
There was a problem hiding this comment.
repository.split("/") assumes GITHUB_REPOSITORY is exactly owner/repo. If it’s malformed (e.g., missing /), this will emit a github.actions.run_url containing undefined. Consider validating the split (or using an existing repo-slug parser) and only adding the attribute when both owner and repo are present. Also consider normalizing GITHUB_SERVER_URL (trim trailing /) before URL construction to avoid // in the emitted run URL.
|
|
||
| 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" }, | ||
| }); |
There was a problem hiding this comment.
These tests set/delete process.env.GITHUB_SERVER_URL, but the sendJobSetupSpan suite’s env save/restore list doesn’t include GITHUB_SERVER_URL, so the mutation can leak into later tests (including other files in the same Vitest worker). Add GITHUB_SERVER_URL to the suite’s envKeys (and restore it in afterEach), or save/restore it locally in each test.
This issue also appears on line 1151 of the same file.
| 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" }, | |
| }); | |
| const originalGithubServerUrl = process.env.GITHUB_SERVER_URL; | |
| try { | |
| 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" }, | |
| }); | |
| } finally { | |
| if (originalGithubServerUrl === undefined) { | |
| delete process.env.GITHUB_SERVER_URL; | |
| } else { | |
| process.env.GITHUB_SERVER_URL = originalGithubServerUrl; | |
| } | |
| } | |
| }); | |
| 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); | |
| const originalGithubServerUrl = process.env.GITHUB_SERVER_URL; | |
| try { | |
| 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" }, | |
| }); | |
| } finally { | |
| if (originalGithubServerUrl === undefined) { | |
| delete process.env.GITHUB_SERVER_URL; | |
| } else { | |
| process.env.GITHUB_SERVER_URL = originalGithubServerUrl; | |
| } | |
| } |
Both
sendJobSetupSpanandsendJobConclusionSpanemittedgithub.repositoryandgithub.run_idas separate resource attributes but never a precomputed run URL — requiring manual assembly in trace UIs and producing wrong URLs on GHES whereGITHUB_SERVER_URL≠https://github.com.Changes
send_otlp_span.cjs: In bothsendJobSetupSpanandsendJobConclusionSpan, readsGITHUB_SERVER_URL(fallback:https://github.com) and appendsgithub.actions.run_urltoresourceAttributeswhen bothrepositoryandrunIdare non-empty:send_otlp_span.test.cjs: Adds 6 new tests (3 per function) covering default URL construction, customGITHUB_SERVER_URL(GHES), and omission when either field is absent.