repomatic.github.ci_status module

Which CI jobs are red, and which of those actually gate a merge.

repomatic names the jobs it generates, prefixing each matrix cell with a glyph that records whether the cell is allowed to fail: for a required one, ⁉️ for a probe running under continue-on-error. Reading that back was left to whoever was watching CI, and it is easy to get wrong in three specific ways this module exists to settle:

  • The glyph, not the position. Job names differ in shape across workflows: tests.yaml emits ubuntu-26.04 / py3.10 while the release engine emits workflow / ubuntu-26.04, abc1234 build. Anything that splits on " / " and reads a fixed field strips the glyph off one of the two and files a required red as a probe, which reads as green.

  • Jobs, not the run. A run’s own conclusion is success while a continue-on-error probe inside it crashed, and its status still reads queued while a dozen of its jobs have already finished. Neither answers “is anything broken”.

  • A run that failed around its jobs. A failure conclusion with no failed job is a workflow-level error: an invalid strategy.matrix expression, malformed YAML, a missing secret. There is no job log to read, and treating it as benign is how a persistently red workflow gets written off as a known artifact.

A job carrying no stability glyph is required. That covers every non-matrix job (1️⃣ Run-once tests, 📦 Package install, 🛡️ Lint types), where the absence of a marker means the job was never optional rather than that its status is unknown. Only the two stability glyphs count: a job name may carry any other emoji and still be required, which is why the test is for ⁉️ specifically rather than for a decorated name.

repomatic.github.ci_status.BATCH_RUN_LIMIT = 100

Runs fetched by the branch-wide listing read_ci_status() starts with.

Deep enough that every monitored workflow’s newest run on a freshly pushed branch sits inside it. A workflow whose newest run is older than the window is not misreported: it falls back to its own latest_run() query.

repomatic.github.ci_status.STABLE_GLYPH = '✅'

Marks a matrix cell that must pass. See UNSTABLE_GLYPH.

repomatic.github.ci_status.UNSTABLE_GLYPH = '⁉️'

Marks a matrix cell running under continue-on-error.

A red one never gates a merge, which is exactly why it has to be told apart from a required cell rather than counted with it. A release still fixes what it can: see claude.md on the genuinely-green goal.

repomatic.github.ci_status.TERMINAL_STATUSES = frozenset({'completed'})

Job statuses meaning the job will not change again.

repomatic.github.ci_status.CI_STATUS_HEADER_DEFS: tuple[tuple[str, str], ...] = (('Workflow', 'workflow'), ('Commit', 'commit'), ('Run status', 'run-status'), ('Verdict', 'verdict'))

Column definitions for the ci-status table.

class repomatic.github.ci_status.JobStatus(name, status, conclusion)[source]

Bases: object

One job of one workflow run.

name: str

The job’s name, glyph included.

status: str

queued, in_progress or completed.

conclusion: str

success, failure, cancelled, skipped, or empty while running.

property required: bool

Whether a failure here gates a merge.

Looks for the glyph anywhere in the raw name rather than at its start. The two shapes disagree on where it sits: tests.yaml leads with it (⁉️ ubuntu-26.04 / py3.15-dev) while the release engine prefixes the workflow first (release / ⁉️ windows-11-arm, abc1234 build). A leading-position test passes the first and silently files the second as required; splitting on " / " gets it wrong the other way round. Containment is the one form both satisfy, and the templates emit exactly one glyph per name.

property failed: bool

Whether this job reached a failing conclusion.

property running: bool

Whether this job has yet to reach a terminal state.

class repomatic.github.ci_status.RunStatus(workflow, run_id, head_sha, status, conclusion, jobs=())[source]

Bases: object

The latest run of one workflow on one branch.

workflow: str

Workflow name, as GitHub reports it.

run_id: int

Numeric run ID, for gh run view.

head_sha: str

Commit the run was created for.

status: str

The run’s own status. Lags its jobs, so it never gates anything here.

conclusion: str

The run’s own conclusion, empty while it is still going.

jobs: tuple[JobStatus, ...] = ()

Every job of the run.

property failed_required: tuple[JobStatus, ...]

Failing jobs that gate a merge.

property failed_probes: tuple[JobStatus, ...]

Failing jobs allowed to fail.

property running_jobs: tuple[JobStatus, ...]

Jobs that have not settled yet.

property workflow_level_failure: bool

Whether the run failed around its jobs rather than inside one.

No job log explains this one: read the run’s error annotations and fix the workflow itself.

property blocking: bool

Whether this run holds up a merge.

property verdict: str

One-phrase outcome, for the table’s last column.

class repomatic.github.ci_status.CIStatus(branch, runs=<factory>)[source]

Bases: object

Every monitored workflow’s latest run on a branch.

branch: str

Branch the runs were read from.

runs: list[RunStatus]

One entry per workflow that has a run, newest first.

property blocking: list[RunStatus]

Runs holding up a merge.

property settled: bool

Whether every run reached a terminal state.

repomatic.github.ci_status.monitored_workflows(workflow_dir)[source]

Every workflow a push to the default branch can start.

Derived from the tree rather than listed by hand, so a workflow added later is watched without anyone remembering to add it here. A reusable workflow is excluded: it has no runs of its own, only the ones its callers create.

Parameters:

workflow_dir (Path) – Directory holding the workflow files.

Return type:

list[str]

Returns:

Workflow filenames, sorted.

repomatic.github.ci_status.latest_run(workflow, branch)[source]

Read a workflow’s most recent run on branch, jobs included.

Parameters:
  • workflow (str) – Workflow filename, like tests.yaml.

  • branch (str) – Branch to read runs from.

Return type:

RunStatus | None

Returns:

The run, or None when the workflow has none. An empty listing is not proof the workflow was filtered out: GitHub can sit on a push event for hours before materializing a run.

repomatic.github.ci_status.read_ci_status(workflows, branch)[source]

Read the latest run of each workflow on branch.

Batched: one branch-wide run listing locates the newest run of every recently active workflow, then one gh run view per run reads its jobs, so a poll costs 2 + N calls where the per-workflow loop cost 2 per workflow. A workflow whose newest run is older than the listing window falls back to its own latest_run() query, so the batching never costs correctness.

Parameters:
  • workflows (Iterable[str]) – Workflow filenames to read.

  • branch (str) – Branch to read runs from.

Return type:

CIStatus

Returns:

The collected status.