neosourceDocs
Search docs

List every workflow run for one commit (across workflows)

GET/api/repos/{owner}/{repo}/commits/{sha}/workflow-runs

listCommitWorkflowRuns

All runs whose triggering event pointed at this exact commit (push tip / PR head), newest-first, each with a compact per-job status summary — the commit-status page's actions view. Manual and scheduled runs carry no trigger commit and never appear here.

Authentication optional — this operation also accepts anonymous requests. When authenticating, use a bearer token or a session cookie.

curl

curl -X GET 'https://neosource.dev/api/repos/OWNER/REPO/commits/SHA/workflow-runs' \
  -H 'Authorization: Bearer $NEOSOURCE_TOKEN'

fetch

fetch("https://neosource.dev/api/repos/OWNER/REPO/commits/SHA/workflow-runs", {
  method: "GET",
  headers: {
    Authorization: "Bearer $NEOSOURCE_TOKEN",
  },
});

Path parameters

ownerrequired

Repository owner or organization slug.

string

reporequired

Repository name.

string

sharequired

The 40-hex SHA-1 commit object ID.

string

ownerrequired

string

reporequired

string

sharequired

string

Responses

200Runs for the commit

application/json

CommitWorkflowRunsResponse

object

All workflow runs (across every workflow) whose triggering event pointed at one commit sha, newest-first — the commit-status page's actions list (`GET /api/repos/{owner}/{repo}/commits/{sha}/workflow-runs`).

runsrequired

array

items

CommitWorkflowRun

object

One run in the per-commit view: the run plus a summary of its jobs.

jobsrequired

array

Every job of the run, in job order, with just id/key/status.

Standard errors

Bodies documented once for the whole API — see standard errors.

  • 400Bad Request — one of: invalid_input
  • 403Forbidden — one of: forbidden
  • 404Not Found — one of: not_found
  • 423Locked — one of: busy
  • 429Rate limited — retry after the `Retry-After` header
  • 500Internal server error
  • 503Service temporarily unavailable / at capacity — retry after the `Retry-After` header
  • 504Gateway timeout — the request exceeded the server's handling budget

Schemas

Referenced above. Listed here rather than expanded inline, so the same definition is not repeated at every level.

CommitJobSummary

object

Compact per-job status shown in the per-commit view — enough to render a job-status badge row without fetching each run's full job list.

idrequired

string

job_keyrequired

string

statusrequired

WorkflowRunJobStatus

string

Lifecycle of an individual job inside a run. Distinct from the run-level status because `needs:` ordering means some jobs are `Waiting` while others are `Running`. The serde `snake_case` strings are the wire vocabulary the API emits (pinned by the `enum_wire_consistency` tests).

"waiting""ready""running""cancelling""succeeded""failed""cancelled""skipped"

WorkflowRunResponse

object

actor_handle

string | null

Handle of the account whose action triggered the run (the pusher, the PR author, the manual dispatcher). `null` for actor-less causes (schedule) and on write receipts that skip the lookup (enqueue).

attemptrequired

integerint32

Re-run attempt (GHA's `github.run_attempt`), starting at 1. A re-run creates a new run that keeps `run_number` and increments this.

branch

string | null

The bookmark (branch) a `push` or ref-aware manual run targeted; `null` for other events and legacy manual rows.

commit_summary

string | null

First line of the trigger commit's message, when that commit is in the metadata projection. Populated on list/detail reads; `null` on write receipts and causes without a pinned commit.

enqueued_atrequired

integerint64

eventrequired

WorkflowRunEvent

string

A [`WorkflowRunCause`] with its payload stripped — the closed event vocabulary run list rows surface (`WorkflowRunResponse.event`). The serde `snake_case` strings are the wire vocabulary (pinned by the `enum_wire_consistency` tests).

"push""change""manual""schedule""pull_request""merge_group"

finished_at

integer | nullint64

idrequired

string

previous_attempt_id

string | null

The run this one is a re-run of (UUID hex), or `null` for a first attempt. Lets the UI link to the previous attempt.

pull_request_number

integer | nullint32

The pull request number for `pull_request` runs.

repo_idrequired

string

run_numberrequired

integerint64

Per-workflow monotonic ordinal (GHA's `github.run_number`). Run 1 of a workflow is 1, the next is 2, etc. Surfaced to steps and used by the runner to populate `${{ github.run_number }}`. Re-run attempts of one logical run share this number.

started_at

integer | nullint64

statusrequired

WorkflowRunStatus

string

Lifecycle of a workflow run. Mirrors the GHA-shaped surface that the SPA renders. The serde `snake_case` strings are the wire vocabulary the API emits (pinned by the `enum_wire_consistency` tests).

"pending_approval""queued""quota_blocked""concurrency_blocked""running""cancelling""succeeded""failed""cancelled"

test_summary
one of
  • null

  • WorkflowRunTestSummary

    object

    Per-run test results parsed from a `JUnit` report artifact at run completion. Counts are individual testcases.

    failedrequired

    integerint32

    failuresrequired

    array

    First N failing tests (server caps the stored list).

    items

    WorkflowRunTestFailure

    object

    One failing test out of the run's `JUnit` report.

    message

    string | null

    The failure/error message attribute, when present.

    namerequired

    string

    Test identity: `classname::name` when the report carries a classname, else the bare testcase name.

    passedrequired

    integerint32

    skippedrequired

    integerint32

    totalrequired

    integerint32

toolchain_manifest
one of
  • null

  • ToolchainManifest

    object

    Everything one run resolved, keyed by [`ToolchainKey::storage_key`]. Written once per run and read by every later reader — the second matrix leg, a re-run, a replay. Authoritative over the constraint that produced it: a re-run re-materializes these artifact identities rather than asking the catalog again (ADR 0083 §2).

    catalog_version

    string | null

    sha256 of the catalog file that produced these entries. **Provenance only, never a lookup key.** The catalog is compiled into the binary, so a server running catalog C′ cannot evaluate against pinned catalog C — the rows are not in the process. Pinning a hash you cannot look up is a promise the deployment model cannot keep, which is why authority sits on the OUTPUT (ADR 0083 §2).

    entries

    object

    `storage_key` → what it resolved to.

    additional properties

    ResolvedToolchain

    object

    What the catalog answered, and what a re-run re-materializes.

    artifactrequired
    ToolArtifact
    toolrequired

    string

    Canonical tool name (aliases resolve to it), or the raw name for a passthrough installable.

    variant

    string | null

    Distribution/vendor where the tool has one (`temurin`, `zulu`, …).

    versionrequired

    string

    The concrete version. **Open vocabulary, deliberately `String`** — the set of version strings is unbounded by construction, so this is the documented exception to the closed-set-DTO-enum rule.

    unreadable

    object

    `storage_key` → why a tolerant reader could not interpret that entry. **Populated only by a reader, never by the server.** The server writes what it can serialize, by construction, so this is empty in every row of `workflow_runs.toolchain_manifest`. A reader fills it in when it meets a [`ToolArtifact`] kind it predates and sets that one entry aside rather than failing the whole payload. A missing key and an unreadable key are NOT the same state: missing takes the late-bound resolve route and, failing that, an approximating local table; unreadable must REFUSE at the point of use. A job that never asks for an unreadable key runs untouched — that is what makes setting one entry aside safe at all. Additive on the wire (`default`, and skipped when empty), so an older reader ignores it and the common empty case costs nothing.

    additional properties

    string

trigger_commit

string | null

Exact commit (40-char git hex) the run executes: push tip, PR head, or the CAS-consistent branch target of a ref-aware manual dispatch. `null` only for legacy manual rows and causes without a commit target (such as schedules). `neo push --runner` polls on this to find a push's runs.

workflow_idrequired

string

ToolArtifact

Where a resolved toolchain's bytes come from.

one of
  • object

    A bare nixpkgs attribute at a pinned revision. One row serves every system; the ref may differ per version row, which is how one platform serves Node 20 and Node 26 at once (ADR 0075 dynamic routing is what makes that affordable).

    attrrequired

    string

    kindrequired

    string

    "nix"

    nixpkgs_refrequired

    string

  • object

    An upstream archive, fetched once platform-side, verified against an upstream-published checksum and stored content-addressed. The digest is the identity; the URL is provenance.

    kindrequired

    string

    "dist"

    layoutrequired

    DistLayout

    string

    How a dist archive unpacks into the tool-cache root.

    "strip_one""flat"

    sha256required

    string

    urlrequired

    string

  • object

    A flake ref or a bare attribute with no catalog row — ADR 0057's escape hatch. Recorded as a raw installable rather than a tool version, because that is honestly all we know about it.

    installablerequired

    string

    kindrequired

    string

    "passthrough"

  • A bare nixpkgs store path (ADR 0101 §1). Materializing it is SUBSTITUTION, not evaluation: no tree fetch, no eval, no `[refs]` entry, no fleet rollout. 289,521 pairs cost the resolver what 2 cost. A validated payload rather than three loose fields: both of its invariants are enforced by [`StorePathArtifact`], on construction and on deserialize alike. The wire form is unchanged — serde flattens an internally-tagged newtype variant — so this is still `{"kind":"store_path","store_path":…,"outputs":[…],…}`.

    all of
    • StorePathArtifact

      object

      The `store_path` artifact's payload, with its invariants enforced. Two of them, and neither was a type before: 1. **`store_path` is a well-formed store path UNDER THE REAL STORE.** It deserializes straight out of `workflow_runs.toolchain_manifest` jsonb and [`ToolchainManifest::has_store_path`] is the agent's whole authorization surface, so as a raw `String` this answered "recorded" for anything that byte-matched a recorded entry — and left the grammar as the daemon's job alone rather than a property of the value. It takes BOTH halves to be that claim, and only the first was a type before: [`NixStorePath`] carries the GRAMMAR and is root-agnostic on purpose (the daemon's root is env-configurable and its own tests point it at a directory of their own), so `/etc/<hash>-passwd` is a shaped path and that type accepts it by design. [`StorePathArtifact::new`] pins the ROOT to [`NixStoreRoot::default`], on construction and on deserialize alike, which is what makes a recorded artifact naming anything outside the real store unconstructable. 2. **`bin_output` is one of `outputs`.** The `deno` → `denort` lesson is that which output carries `bin/` is index-ANSWERED, never guessed, and an answer naming an output the derivation did not have is not an answer. That was prose in a doc comment; it is checked here. `revision` stays an unvalidated `String` on purpose, and the asymmetry is the point: it is provenance only. No decision is made on it — that is exactly why [`ToolArtifact::nixpkgs_ref`] does not return it — so a malformed revision misleads a reader without routing anything, while a malformed store path is an authorization answer.

      bin_outputrequired

      string

      Which output `store_path` is. Index-answered, never guessed — the `deno` → `denort` lesson says a name heuristic is not enough. A member of `outputs` by construction.

      outputsrequired

      array

      Every output the derivation had, for provenance only.

      items

      string

      revisionrequired

      string

      The revision the index chose (ADR 0101 §2a records it "beside the store path"). Deliberately NOT named `nixpkgs_ref` and deliberately NOT returned by [`ToolArtifact::nixpkgs_ref`]: that accessor drives `group_by_ref` and the daemon's attr path on 8080, and a revision leaking there routes a ref that is not in `ALLOWED_REFS` — the 2026-07-31 403 storm.

      store_pathrequired

      NixStorePath

      string

      An absolute, well-formed store path — `<root>/<hash>-<name>` for SOME valid [`NixStoreRoot`] — validated on construction AND on deserialize. **The grammar is a property of the VALUE, not a step some caller remembers.** `ToolArtifact::StorePath` is deserialized straight out of `workflow_runs.toolchain_manifest` jsonb and is the agent's whole authorization surface (`ToolchainManifest::has_store_path`), so a raw `String` there would let `/etc/passwd` answer "recorded" the moment it byte-matched a recorded entry, and would leave the grammar as the daemon's job alone. Validating in `Deserialize` too is the half that matters: an invariant that holds only for freshly-built values is not an invariant, and every value on this path arrives from a database. Deliberately ROOT-INDEPENDENT: whether a path is under the root *this* process serves is [`store_path_leaf`]'s question, and that root is env-configurable. This type answers the other one — is it shaped like a store path at all. Which means the grammar alone is NOT the authorization answer, and the doc must not be read as claiming it is: `/etc` is a root [`NixStoreRoot`] admits, so `/etc/<hash>-passwd` is a well-formed value of this type. Pinning a RECORDED artifact to the real store is [`crate::model::toolchain::StorePathArtifact::new`]'s clause, one layer up, where the recording happens.

    • object

      kindrequired

      string

      "store_path"