📡 OTel Instrumentation Improvement: distinguish partial-success runs in gh-aw.run.status
Analysis Date: 2026-05-20
Priority: Medium
Effort: Small (< 2h)
Problem
When the agent process concludes with GH_AW_AGENT_CONCLUSION=success but agent_output.json contains non-empty errors (e.g. a safe-output failed to push, a tool call returned an error message, a sub-action reported a non-fatal failure), the conclusion span at actions/setup/js/send_otlp_span.cjs:1719–1745 is emitted with:
gh-aw.run.status = "success"
status.code = 1 (STATUS_CODE_OK)
gh-aw.error.count > 0
exception events for each error
The error data is captured (good), but the summary signal says "healthy". As a result, a DevOps engineer running the standard OTel error query in Sentry / Grafana Tempo / Datadog (status.code = ERROR or gh-aw.run.status IN (failure, timeout, cancelled)) cannot answer: "Show me all runs that finished but had operational errors." They have to add a custom gh-aw.error.count > 0 clause, which is not part of any standard out-of-the-box error dashboard.
This is a deliberate design choice — see actions/setup/js/send_otlp_span.test.cjs:4571 ("does not override explicit GH_AW_AGENT_CONCLUSION=success even when outputErrors exist"), which asserts the current behavior. The proposal here is non-breaking: keep status.code = 1, but extend the gh-aw.run.status taxonomy with a new "partial_success" value so partial-success runs are distinguishable from clean-success runs without rewriting alerting rules.
Why This Matters (DevOps Perspective)
The gh-aw.run.status attribute is the single most useful low-cardinality slice for dashboards — it's already used by gh-aw.run.status=timeout queries to separate timeouts from generic failures (see send_otlp_span.cjs:1723–1726). Adding partial_success extends that same pattern to cover the "finished but had errors" case.
Concrete operational scenarios this unblocks:
- Sentry: an alert rule "any gh-aw run with non-empty errors in the last 1h" today either misses partial successes (if it filters on issue status) or fires too broadly (if it filters on
gh-aw.error.count).
- Grafana / Tempo: the canonical TraceQL filter
{ resource.service.name = "gh-aw" && span.gh-aw.run.status != "success" } would surface partial successes alongside failures — currently they slip through.
- MTTR: when an on-call engineer sees a successful run that produced no artifacts (because all safe-outputs errored), they currently have to inspect the JSONL artifact to discover the failure. A
partial_success status surfaces it in the first dashboard glance.
This is the classic "misleading data" failure mode — the data exists but is not surfaced in the operational summary signal.
Current Behavior
// actions/setup/js/send_otlp_span.cjs lines 1694–1745
const isAgentTimedOut = agentConclusion === "timed_out";
const isAgentFailure = agentConclusion === "failure" || isAgentTimedOut;
const isAgentCancelled = agentConclusion === "cancelled";
const isAgentNonOK = isAgentFailure || isAgentCancelled;
let statusCode = isAgentNonOK ? 2 : 1;
let statusMessage;
// ...
let runStatus = "success";
const rawRunStatus = agentConclusion || workflowRunConclusion;
if (rawRunStatus === "cancelled") {
runStatus = "cancelled";
} else if (rawRunStatus === "timed_out") {
runStatus = "timeout";
} else if (rawRunStatus === "failure") {
runStatus = "failure";
}
// Only fires when rawRunStatus is empty — not when it is explicitly "success":
if (!rawRunStatus && outputErrors.length > 0) {
runStatus = "failure";
statusCode = 2;
statusMessage = (errorMessages.length > 0 ? `errors detected: ${errorMessages[0]}` : "errors detected").slice(0, 256);
}
if (isAgentFailure && errorMessages.length > 0) {
statusMessage = `agent ${agentConclusion}: ${errorMessages[0]}`.slice(0, 256);
}
// ...
attributes.push(buildAttr("gh-aw.run.status", runStatus));
attributes.push(buildAttr("gh-aw.error_count", outputErrors.length));
The test at send_otlp_span.test.cjs:4571 pins this behavior:
it("does not override explicit GH_AW_AGENT_CONCLUSION=success even when outputErrors exist", async () => {
process.env.GH_AW_AGENT_CONCLUSION = "success";
// ... agent_output.json with { errors: [{ message: "non-fatal warning error" }] }
expect(attrs["gh-aw.run.status"]).toBe("success");
expect(span.status.code).toBe(1);
});
Proposed Change
Add partial_success as a new gh-aw.run.status value. Keep status.code = 1 (non-breaking).
// actions/setup/js/send_otlp_span.cjs (replace the block at lines 1719–1738)
let runStatus = "success";
const rawRunStatus = agentConclusion || workflowRunConclusion;
if (rawRunStatus === "cancelled") {
runStatus = "cancelled";
} else if (rawRunStatus === "timed_out") {
runStatus = "timeout";
} else if (rawRunStatus === "failure") {
runStatus = "failure";
} else if (rawRunStatus === "success" && outputErrors.length > 0) {
// Agent process concluded successfully, but reported non-fatal errors via
// agent_output.json (e.g. a safe-output failed, a tool call returned an error).
// Keep status.code = OK so the team's explicit "success when conclusion=success"
// contract is preserved, but mark the run.status distinctly so operators can
// query for partial successes without parsing per-span error attributes.
runStatus = "partial_success";
}
if (!rawRunStatus && outputErrors.length > 0) {
runStatus = "failure";
statusCode = 2;
statusMessage = (errorMessages.length > 0 ? `errors detected: ${errorMessages[0]}` : "errors detected").slice(0, 256);
}
Expected Outcome
After this change:
- In Grafana / Honeycomb / Datadog: a single low-cardinality filter
gh-aw.run.status IN (failure, timeout, cancelled, partial_success) surfaces every run that needs attention.
- In Sentry: alert rules can be tuned per status — page on
failure/timeout, ticket on partial_success, ignore cancelled.
- In the JSONL mirror:
jq 'select(.attributes[] | select(.key == "gh-aw.run.status") | .value.stringValue == "partial_success")' becomes the canonical way to find these runs.
- For on-call engineers: a run with
gh-aw.run.status=partial_success is immediately recognizable as "agent ran fine but produced errors" without having to inspect gh-aw.error.messages or the exception events.
Behavior preserved (non-breaking):
span.status.code = 1 (OK) when GH_AW_AGENT_CONCLUSION=success, matching the existing contract.
- All existing
gh-aw.run.status values (success, failure, timeout, cancelled) keep their current semantics.
- Exception events and
gh-aw.error.* attributes are unchanged.
Evidence from Live Sentry Data
Live Sentry MCP tooling was not available in this run (the sentry MCP server's tool registry was empty: /home/runner/work/_temp/gh-aw/mcp-cli/tools/sentry.json contained [], and sentry find_organizations returned Error [-32602]: unknown tool). Analysis was grounded in the local OTLP JSONL mirror plus static code inspection.
Live span sampled (/tmp/gh-aw/otel.jsonl, this run):
traceId: 5356be426197a28ba11634bcba38fb76
name: gh-aw.agent.setup
parentSpanId: 61e5c778ff4e3db2 (trace continuity intact — setup is parented under the activation span)
status.code: 1 (OK)
- Resource attributes present:
service.name, service.version=2.1.142, github.repository, github.run_id, github.run_attempt, github.event_name, github.workflow_ref, deployment.environment=production, runner.os/arch/name/environment. All of the candidate "missing" resource attributes (service.version, github.event_name, github.run_id, deployment.environment) are already populated — the resource-attribute layer is in excellent shape.
The live span is for an in-flight run that has not reached the conclusion phase, so this specific span cannot itself exhibit the gap. The gap was confirmed instead by:
- Tracing the code path in
send_otlp_span.cjs:1719–1745.
- The pinning test at
send_otlp_span.test.cjs:4571 ("does not override explicit GH_AW_AGENT_CONCLUSION=success even when outputErrors exist"), whose comment explicitly labels the input as a non-fatal warning error.
- The asymmetry between the
!rawRunStatus && outputErrors.length > 0 branch (which sets runStatus="failure", statusCode=2) and the rawRunStatus==="success" && outputErrors.length > 0 case (which does nothing).
Implementation Steps
Related Files
actions/setup/js/send_otlp_span.cjs (lines 1719–1745)
actions/setup/js/send_otlp_span.test.cjs (lines 3977–4003 and 4571–4592)
actions/setup/js/action_conclusion_otlp.cjs (caller — no changes needed)
docs/src/content/docs/guides/custom-otlp-attributes.md (if gh-aw.run.status taxonomy is documented)
Generated by the Daily OTel Instrumentation Advisor workflow
Generated by 📊 Daily OTel Instrumentation Advisor · ● 12.3M · ◷
📡 OTel Instrumentation Improvement: distinguish partial-success runs in
gh-aw.run.statusAnalysis Date: 2026-05-20
Priority: Medium
Effort: Small (< 2h)
Problem
When the agent process concludes with
GH_AW_AGENT_CONCLUSION=successbutagent_output.jsoncontains non-emptyerrors(e.g. a safe-output failed to push, a tool call returned an error message, a sub-action reported a non-fatal failure), the conclusion span atactions/setup/js/send_otlp_span.cjs:1719–1745is emitted with:gh-aw.run.status = "success"status.code = 1(STATUS_CODE_OK)gh-aw.error.count> 0exceptionevents for each errorThe error data is captured (good), but the summary signal says "healthy". As a result, a DevOps engineer running the standard OTel error query in Sentry / Grafana Tempo / Datadog (
status.code = ERRORorgh-aw.run.status IN (failure, timeout, cancelled)) cannot answer: "Show me all runs that finished but had operational errors." They have to add a customgh-aw.error.count > 0clause, which is not part of any standard out-of-the-box error dashboard.This is a deliberate design choice — see
actions/setup/js/send_otlp_span.test.cjs:4571("does not override explicit GH_AW_AGENT_CONCLUSION=success even when outputErrors exist"), which asserts the current behavior. The proposal here is non-breaking: keepstatus.code = 1, but extend thegh-aw.run.statustaxonomy with a new"partial_success"value so partial-success runs are distinguishable from clean-success runs without rewriting alerting rules.Why This Matters (DevOps Perspective)
The
gh-aw.run.statusattribute is the single most useful low-cardinality slice for dashboards — it's already used bygh-aw.run.status=timeoutqueries to separate timeouts from generic failures (seesend_otlp_span.cjs:1723–1726). Addingpartial_successextends that same pattern to cover the "finished but had errors" case.Concrete operational scenarios this unblocks:
gh-aw.error.count).{ resource.service.name = "gh-aw" && span.gh-aw.run.status != "success" }would surface partial successes alongside failures — currently they slip through.partial_successstatus surfaces it in the first dashboard glance.This is the classic "misleading data" failure mode — the data exists but is not surfaced in the operational summary signal.
Current Behavior
The test at
send_otlp_span.test.cjs:4571pins this behavior:Proposed Change
Add
partial_successas a newgh-aw.run.statusvalue. Keepstatus.code = 1(non-breaking).Expected Outcome
After this change:
gh-aw.run.status IN (failure, timeout, cancelled, partial_success)surfaces every run that needs attention.failure/timeout, ticket onpartial_success, ignorecancelled.jq 'select(.attributes[] | select(.key == "gh-aw.run.status") | .value.stringValue == "partial_success")'becomes the canonical way to find these runs.gh-aw.run.status=partial_successis immediately recognizable as "agent ran fine but produced errors" without having to inspectgh-aw.error.messagesor the exception events.Behavior preserved (non-breaking):
span.status.code = 1(OK) whenGH_AW_AGENT_CONCLUSION=success, matching the existing contract.gh-aw.run.statusvalues (success,failure,timeout,cancelled) keep their current semantics.gh-aw.error.*attributes are unchanged.Evidence from Live Sentry Data
Live Sentry MCP tooling was not available in this run (the
sentryMCP server's tool registry was empty:/home/runner/work/_temp/gh-aw/mcp-cli/tools/sentry.jsoncontained[], andsentry find_organizationsreturnedError [-32602]: unknown tool). Analysis was grounded in the local OTLP JSONL mirror plus static code inspection.Live span sampled (
/tmp/gh-aw/otel.jsonl, this run):traceId:5356be426197a28ba11634bcba38fb76name:gh-aw.agent.setupparentSpanId:61e5c778ff4e3db2(trace continuity intact — setup is parented under the activation span)status.code: 1 (OK)service.name,service.version=2.1.142,github.repository,github.run_id,github.run_attempt,github.event_name,github.workflow_ref,deployment.environment=production,runner.os/arch/name/environment. All of the candidate "missing" resource attributes (service.version,github.event_name,github.run_id,deployment.environment) are already populated — the resource-attribute layer is in excellent shape.The live span is for an in-flight run that has not reached the conclusion phase, so this specific span cannot itself exhibit the gap. The gap was confirmed instead by:
send_otlp_span.cjs:1719–1745.send_otlp_span.test.cjs:4571("does not override explicitGH_AW_AGENT_CONCLUSION=successeven when outputErrors exist"), whose comment explicitly labels the input as anon-fatal warning error.!rawRunStatus && outputErrors.length > 0branch (which setsrunStatus="failure",statusCode=2) and therawRunStatus==="success" && outputErrors.length > 0case (which does nothing).Implementation Steps
actions/setup/js/send_otlp_span.cjsto add thepartial_successbranch shown in the Proposed Change section above (lines ~1719–1738).actions/setup/js/send_otlp_span.test.cjs:4571so thedoes not override explicit GH_AW_AGENT_CONCLUSION=success even when outputErrors existtest now assertsgh-aw.run.status === "partial_success"andstatus.code === 1.agent_conclusion=successwith no errors still yieldsgh-aw.run.status === "success".cd actions/setup/js && npx vitest run send_otlp_span.test.cjsandmake test-unitto confirm tests pass.make fmt.docs/src/content/docs/guides/custom-otlp-attributes.mdifgh-aw.run.statusvalues are documented there.Related Files
actions/setup/js/send_otlp_span.cjs(lines 1719–1745)actions/setup/js/send_otlp_span.test.cjs(lines 3977–4003 and 4571–4592)actions/setup/js/action_conclusion_otlp.cjs(caller — no changes needed)docs/src/content/docs/guides/custom-otlp-attributes.md(ifgh-aw.run.statustaxonomy is documented)Generated by the Daily OTel Instrumentation Advisor workflow