Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d40df62
Initial plan
Copilot Apr 4, 2026
88889a9
feat: add OTLP trace export configuration to observability section (#…
Copilot Apr 4, 2026
233b74c
feat: add send_otlp_span.cjs and instrument action setup with job-nam…
Copilot Apr 4, 2026
c890c53
fix: address code review feedback on send_otlp_span.cjs and test file
Copilot Apr 4, 2026
ffd3965
feat: add trace-id input/output to actions/setup for cross-job span c…
Copilot Apr 4, 2026
6a3a6b9
fix: validate trace-id format, add isValidTraceId helper + tests
Copilot Apr 4, 2026
4a40521
fix: validate options.traceId, normalize uppercase input, reuse isVal…
Copilot Apr 4, 2026
965d5d7
feat: scope name gh-aw + version, retry/warn sendOTLPSpan, conclusion…
Copilot Apr 4, 2026
0b4b16e
feat: add OTEL_EXPORTER_OTLP_HEADERS support to observability.otlp co…
Copilot Apr 4, 2026
d3191e0
fix: decode before trim in parseOTLPHeaders, clarify eqIdx comment
Copilot Apr 4, 2026
073a79e
feat: propagate trace/span IDs via GITHUB_ENV for 1-trace-per-run, 1-…
Copilot Apr 4, 2026
4f094e4
fix: simplify traceId resolution logic, normalize spelling to America…
Copilot Apr 4, 2026
e3ae913
refactor: rename GH_AW_TRACE_ID/GH_AW_PARENT_SPAN_ID to GITHUB_AW_OTE…
Copilot Apr 4, 2026
a361f5c
feat: mirror every OTLP span to /tmp/gh-aw/otel.jsonl for artifact in…
Copilot Apr 4, 2026
1c1b54f
feat: configure smoke-copilot with OTLP secrets; fix raw frontmatter …
Copilot Apr 4, 2026
571fef3
fix: add comment to Engine any field; fix em dash in comment
Copilot Apr 4, 2026
27383e7
merge: merge origin/main and recompile all workflows
Copilot Apr 4, 2026
6617b55
refactor: move OTLP conclusion span to action post step
Copilot Apr 4, 2026
71d8baa
feat: add setup.sh OTLP span + clean.sh post-step for script mode
Copilot Apr 4, 2026
e34ad2f
refactor: extract OTLP setup/conclusion logic into .cjs files
Copilot Apr 4, 2026
9a2c2d7
feat: add logging to OTLP codepaths in .cjs and .sh files
Copilot Apr 4, 2026
310da37
feat: propagate OTLP trace ID through aw_context for composite actions
Copilot Apr 4, 2026
b9438c8
fix: skip OTLP setup span in setup.sh when called from index.js (acti…
Copilot Apr 4, 2026
5214d82
feat: wire activation trace ID through all downstream jobs for unifie…
Copilot Apr 4, 2026
be04457
fix: route all downstream job setup trace IDs through needs.activatio…
Copilot Apr 4, 2026
3ded896
fix: explicitly forward INPUT_TRACE_ID to action_setup_otlp.cjs in se…
Copilot Apr 4, 2026
f93cf7e
fix: always propagate trace-id in action_setup_otlp regardless of OTL…
Copilot Apr 4, 2026
a86b33b
fix: rename rawInputTraceId to inputTraceId, add fallback test
Copilot Apr 4, 2026
340235b
fix: add logging of INPUT_TRACE_ID in action_setup_otlp.cjs
Copilot Apr 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ inputs:
description: 'Install @actions/github for handlers that use a per-handler github-token (creates Octokit via getOctokit)'
required: false
default: 'false'
job-name:
description: 'Name of the job being set up. When OTEL_EXPORTER_OTLP_ENDPOINT is configured, a gh-aw.job.setup span is pushed to the OTLP endpoint.'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The job-name input is well-documented. The trace-id input description is clear about its purpose for cross-job span correlation — nice design for distributed tracing.

required: false
default: ''
trace-id:
description: 'OTLP trace ID (32-character hexadecimal string) to reuse for cross-job span correlation. Pass the trace-id output of the activation job setup step to correlate all job spans under the same trace. When omitted a new trace ID is generated.'
required: false
default: ''
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new trace-id input enables cross-job span correlation via OTLP — good addition for distributed tracing across the setup → activation → agent job chain. Consider documenting the format constraint (32-char hex) in the description for clarity.


outputs:
files_copied:
description: 'Number of files copied'
trace-id:
description: 'The OTLP trace ID used for the gh-aw.job.setup span. Pass this to subsequent job setup steps via the trace-id input to correlate all job spans under a single trace.'

runs:
using: 'node24'
Expand Down
24 changes: 24 additions & 0 deletions actions/setup/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

251 changes: 251 additions & 0 deletions actions/setup/js/send_otlp_span.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// @ts-check
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Smoke test inline review comment — the OTLP span sender looks solid. Consider adding a comment explaining the retry backoff constants (100ms/200ms) for future readers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new send_otlp_span.cjs file looks well-structured. Consider adding a JSDoc comment at the top describing the module purpose and its main exports (sendJobSetupSpan, sendJobConclusionSpan) for better discoverability.

/// <reference types="@actions/github-script" />

const { randomBytes } = require("crypto");

/**
* send_otlp_span.cjs
*
* Sends a single OTLP (OpenTelemetry Protocol) trace span to the configured
* HTTP/JSON endpoint. Used by actions/setup to instrument each job execution
* with basic telemetry.
*
* Design constraints:
* - No-op when OTEL_EXPORTER_OTLP_ENDPOINT is not set (zero overhead).
* - Errors are non-fatal: export failures must never break the workflow.
* - No third-party dependencies: uses only Node built-ins + native fetch.
*/

// ---------------------------------------------------------------------------
// Low-level helpers
// ---------------------------------------------------------------------------

/**
* Generate a random 16-byte trace ID encoded as a 32-character hex string.
* @returns {string}
*/
function generateTraceId() {
return randomBytes(16).toString("hex");
}

/**
* Generate a random 8-byte span ID encoded as a 16-character hex string.
* @returns {string}
*/
function generateSpanId() {
return randomBytes(8).toString("hex");
}

/**
* Convert a Unix timestamp in milliseconds to a nanosecond string suitable for
* OTLP's `startTimeUnixNano` / `endTimeUnixNano` fields.
*
* BigInt arithmetic avoids floating-point precision loss for large timestamps.
*
* @param {number} ms - milliseconds since Unix epoch
* @returns {string} nanoseconds since Unix epoch as a decimal string
*/
function toNanoString(ms) {
return (BigInt(Math.floor(ms)) * 1_000_000n).toString();
}

/**
* Build a single OTLP attribute object in the key-value format expected by the
* OTLP/HTTP JSON wire format.
*
* @param {string} key
* @param {string | number | boolean} value
* @returns {{ key: string, value: object }}
*/
function buildAttr(key, value) {
if (typeof value === "boolean") {
return { key, value: { boolValue: value } };
}
if (typeof value === "number") {
return { key, value: { intValue: value } };
}
return { key, value: { stringValue: String(value) } };
}

// ---------------------------------------------------------------------------
// OTLP payload builder
// ---------------------------------------------------------------------------

/**
* @typedef {Object} OTLPSpanOptions
* @property {string} traceId - 32-char hex trace ID
* @property {string} spanId - 16-char hex span ID
* @property {string} spanName - Human-readable span name
* @property {number} startMs - Span start time (ms since epoch)
* @property {number} endMs - Span end time (ms since epoch)
* @property {string} serviceName - Value for the service.name resource attribute
* @property {Array<{key: string, value: object}>} attributes - Span attributes
*/

/**
* Build an OTLP/HTTP JSON traces payload wrapping a single span.
*
* @param {OTLPSpanOptions} opts
* @returns {object} - Ready to be serialised as JSON and POSTed to `/v1/traces`
*/
function buildOTLPPayload({ traceId, spanId, spanName, startMs, endMs, serviceName, attributes }) {
return {
resourceSpans: [
{
resource: {
attributes: [buildAttr("service.name", serviceName)],
},
scopeSpans: [
{
scope: { name: "gh-aw.setup", version: "1.0.0" },
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@copilot

  • name gh-aw
  • pass current version

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 965d5d7:

  • Scope name changed from "gh-aw.setup" to "gh-aw"
  • Scope version now reads process.env.GH_AW_INFO_VERSION || "unknown" (same env var written by the compiler for every job)

spans: [
{
traceId,
spanId,
name: spanName,
kind: 2, // SPAN_KIND_SERVER
startTimeUnixNano: toNanoString(startMs),
endTimeUnixNano: toNanoString(endMs),
status: { code: 1 }, // STATUS_CODE_OK
attributes,
},
],
},
],
},
],
};
}

// ---------------------------------------------------------------------------
// HTTP transport
// ---------------------------------------------------------------------------

/**
* POST an OTLP traces payload to `{endpoint}/v1/traces`.
*
* @param {string} endpoint - OTLP base URL (e.g. https://traces.example.com:4317)
* @param {object} payload - Serialisable OTLP JSON object
* @returns {Promise<void>}
* @throws {Error} when the server returns a non-2xx status
*/
async function sendOTLPSpan(endpoint, payload) {
const url = endpoint.replace(/\/$/, "") + "/v1/traces";
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`OTLP export failed: HTTP ${response.status} ${response.statusText}`);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@copilot don't throw, warn. Use with retry

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 965d5d7. sendOTLPSpan now:

  • Uses console.warn instead of throwing on non-2xx responses
  • Retries up to 3 times (configurable) with exponential back-off (100 ms, 200 ms) before the final warning

}
}

// ---------------------------------------------------------------------------
// High-level: job setup span
// ---------------------------------------------------------------------------

/**
* Regular expression that matches a valid OTLP trace ID: 32 lowercase hex characters.
* @type {RegExp}
*/
const TRACE_ID_RE = /^[0-9a-f]{32}$/;

/**
* Validate that a string is a well-formed OTLP trace ID (32 lowercase hex chars).
* @param {string} id
* @returns {boolean}
*/
function isValidTraceId(id) {
return TRACE_ID_RE.test(id);
}

/**
* @typedef {Object} SendJobSetupSpanOptions
* @property {number} [startMs] - Override for the span start time (ms). Defaults to `Date.now()`.
* @property {string} [traceId] - Existing trace ID to reuse for cross-job correlation.
* When omitted the value is taken from the `INPUT_TRACE_ID` environment variable (the
* `trace-id` action input); if that is also absent a new random trace ID is generated.
* Pass the `trace-id` output of the activation job setup step to correlate all
* subsequent job spans under the same trace.
*/

/**
* Send a `gh-aw.job.setup` span to the configured OTLP endpoint.
*
* This is designed to be called from `actions/setup/index.js` immediately after
* the setup script completes. It always returns the trace ID so callers can
* expose it as an action output for cross-job correlation — even when
* `OTEL_EXPORTER_OTLP_ENDPOINT` is not set (no span is sent in that case).
* Errors are swallowed so the workflow is never broken by tracing failures.
*
* Environment variables consumed:
* - `OTEL_EXPORTER_OTLP_ENDPOINT` – collector endpoint (required to send anything)
* - `OTEL_SERVICE_NAME` – service name (defaults to "gh-aw")
* - `INPUT_JOB_NAME` – job name passed via the `job-name` action input
* - `INPUT_TRACE_ID` – optional trace ID passed via the `trace-id` action input
* - `GH_AW_INFO_WORKFLOW_NAME` – workflow name injected by the gh-aw compiler
* - `GH_AW_INFO_ENGINE_ID` – engine ID injected by the gh-aw compiler
* - `GITHUB_RUN_ID` – GitHub Actions run ID
* - `GITHUB_ACTOR` – GitHub Actions actor (user / bot)
* - `GITHUB_REPOSITORY` – `owner/repo` string
*
* @param {SendJobSetupSpanOptions} [options]
* @returns {Promise<string>} The trace ID used for the span (generated or passed in).
*/
async function sendJobSetupSpan(options = {}) {
// Resolve the trace ID before the early-return so it is always available as
// an action output regardless of whether OTLP is configured.
// Priority: options.traceId > INPUT_TRACE_ID env var > newly generated ID.
// Invalid (non-hex, wrong-length) values are silently discarded in favour of a new ID.
const rawInputTraceId = (process.env.INPUT_TRACE_ID || "").trim().toLowerCase();
const inputTraceId = isValidTraceId(rawInputTraceId) ? rawInputTraceId : "";
const candidateTraceId = options.traceId || inputTraceId;
const traceId = candidateTraceId && isValidTraceId(candidateTraceId) ? candidateTraceId : generateTraceId();

const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "";
if (!endpoint) {
return traceId;
}

const startMs = options.startMs ?? Date.now();
const endMs = Date.now();

const serviceName = process.env.OTEL_SERVICE_NAME || "gh-aw";
const jobName = process.env.INPUT_JOB_NAME || "";
const workflowName = process.env.GH_AW_INFO_WORKFLOW_NAME || process.env.GITHUB_WORKFLOW || "";
const engineId = process.env.GH_AW_INFO_ENGINE_ID || "";
const runId = process.env.GITHUB_RUN_ID || "";
const actor = process.env.GITHUB_ACTOR || "";
const repository = process.env.GITHUB_REPOSITORY || "";

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)];

if (engineId) {
attributes.push(buildAttr("gh-aw.engine.id", engineId));
}

const payload = buildOTLPPayload({
traceId,
spanId: generateSpanId(),
spanName: "gh-aw.job.setup",
startMs,
endMs,
serviceName,
attributes,
});

await sendOTLPSpan(endpoint, payload);
return traceId;
}

module.exports = {
isValidTraceId,
generateTraceId,
generateSpanId,
toNanoString,
buildAttr,
buildOTLPPayload,
sendOTLPSpan,
sendJobSetupSpan,
};
Loading