Skip to content

feat: opt-in deterministic trace and job span IDs - #219

Open
georgegebbett wants to merge 1 commit into
paper2:mainfrom
Lawhive:deterministic-trace-ids
Open

georgegebbett wants to merge 1 commit into
paper2:mainfrom
Lawhive:deterministic-trace-ids

Conversation

@georgegebbett

Copy link
Copy Markdown

Adds a DETERMINISTIC_TRACE_IDS input. When enabled, the trace ID is derived from the repository, run ID and run attempt, and job span IDs from the job ID, so other tools can attach spans to a workflow run's trace without talking to this action. The derivation is documented in the README.

Adds a DETERMINISTIC_TRACE_IDS input. When enabled, the trace ID is derived
from the repository, run ID and run attempt, and job span IDs from the job ID,
so other tools can attach spans to a workflow run's trace without talking to
this action. The derivation is documented in the README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@paper2 paper2 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good idea. Deriving the trace ID from the run lets another tool join the trace without this action passing the ID over, which fits an action that builds the trace after the workflow finishes.

Deriving the job span IDs as well seems to add more complexity: three public span formulas, the numeric job id, and parenting extra spans under a specific job span. I want to check that this part is actually needed.

If the goal is to parent extra spans under a job, could you describe that case? Which tool emits the span, and does it run inside the job or after the workflow has finished? Which of the three job spans should it use as its parent, and why is sharing the trace ID not enough? How does that tool obtain the numeric job id, as opposed to github.job?

Comment thread README.md
Step spans keep random IDs. For example, to parent a span under a job in shell:

```bash
trace_id=$(printf '%s' "$GITHUB_REPOSITORY:$RUN_ID:$RUN_ATTEMPT" | sha256sum | cut -c1-32)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example does not hash the same key the implementation uses.

workflowTraceId is SHA-256 of repository + : + the workflow run id + : + run_attempt (GITHUB_RUN_ID / GITHUB_RUN_ATTEMPT, not the workflow definition id). jobSpanId is SHA-256 of job: + the numeric Actions job id from the jobs API.

$RUN_ID, $RUN_ATTEMPT, and $JOB_ID are not set by GitHub. GITHUB_JOB / github.job is the YAML job key (build), not that numeric id. Hashing the YAML key, or an empty JOB_ID, produces a different span id, so a child span will not land on this action's job span. The table's <job_id> has the same ambiguity.

Use $GITHUB_REPOSITORY:$GITHUB_RUN_ID:$GITHUB_RUN_ATTEMPT, and job:$JOB_ID only after defining JOB_ID as the numeric job id (jobs[].id; on github.com and GHEC that is ${{ job.check_run_id }}, not github.job). printf '%s' | sha256sum | cut -c1-32 / cut -c1-16 already matches hashId.

test('should keep random IDs unless deterministic IDs are enabled', async () => {
const traceId = await createTrace(workflowRunResults)
expect(traceId).toMatch(/^[0-9a-f]{32}$/)
expect(traceId).not.toBe('9b622c3f42e4e04dcc07e34075e07913')

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nits] It should be wrote :)

Suggested change
expect(traceId).not.toBe('9b622c3f42e4e04dcc07e34075e07913')
// Hard-coded: these are the documented contract other tools compute against.
expect(traceId).not.toBe('9b622c3f42e4e04dcc07e34075e07913')

Comment thread src/traces/span-ids.ts
import { createHash } from 'node:crypto'
import { IdGenerator, RandomIdGenerator } from '@opentelemetry/sdk-trace-base'

export type SpanIds = { traceId?: string; spanId?: string }

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[imo] SpanIds reads like a collection of span IDs, but this is the trace ID and span ID preset for a single startSpan. PresetIds matches withIds / preset.

Comment thread README.md
@georgegebbett

georgegebbett commented Sep 28, 2026 •

Copy link
Copy Markdown
Author

This is a good idea. Deriving the trace ID from the run lets another tool join the trace without this action passing the ID over, which fits an action that builds the trace after the workflow finishes.

Deriving the job span IDs as well seems to add more complexity: three public span formulas, the numeric job id, and parenting extra spans under a specific job span. I want to check that this part is actually needed.

If the goal is to parent extra spans under a job, could you describe that case? Which tool emits the span, and does it run inside the job or after the workflow has finished? Which of the three job spans should it use as its parent, and why is sharing the trace ID not enough? How does that tool obtain the numeric job id, as opposed to github.job?

We have hand-rolled a tool which parses the JUnit reports for our test suites, emits OTel traces, and then hangs them off the emitted spans in the root CI run (see screenshot) - I figured having the max granularity was useful for people with a need for something more specific rather than limiting for our use case specifically - the number of spans emitted etc is static at any rate, this change only makes their IDs predictable, and I figured if someone opts into deterministic IDs they would probably expect them all to be deterministic rather than just some of them

image

In terms of getting the job ID, we are doing the same thing downstream that this action does and calling the GitHub API for the jobs in the run attempt after the fact, having triggered this lookup as part of the workflow completed event which contains the run ID

@paper2

paper2 commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Thank you very much! I understood the intention.

It is a policy to incorporate this change. It is no problem to be determinstic in all spans. Please respond to the comments on the pull request!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants