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 four 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 run that settled without a result. A newer push cancels the run of the commit under it, and a job-level if: skips every job of a run: the bundled tests.yaml does that for a version-bump commit. Neither run has a failed job, and neither says anything about its commit. Reading one as green hides that the matrix never ran on the newest commits.

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. A name that holds both glyphs is the one exception: see JobStatus.required().

repomatic.github.ci_status.TIP_HISTORY_DEPTH = 10

Commits read_ci_status() walks back from the branch tip, tip included.

A push starts most workflows on the tip itself. A workflow whose paths: filter skipped the tip last ran on an earlier commit, which a walk this deep reaches on any active branch. A workflow with no run in the window falls back to latest_run().

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 the repomatic-ship skill 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[ColumnSpec, ...] = (ColumnSpec(id='workflow', label='Workflow', description='', max_width=None, optional=False, sortable=True), ColumnSpec(id='commit', label='Commit', description='', max_width=None, optional=False, sortable=True), ColumnSpec(id='run-status', label='Run status', description='', max_width=None, optional=False, sortable=True), ColumnSpec(id='verdict', label='Verdict', description='', max_width=None, optional=False, sortable=True))

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 expanded name.

A name that holds both glyphs is a matrix GitHub never expanded: the job keeps its name: expression, which spells the two of them. A run stopped before the matrix got its cells leaves one. Some of those cells are required, so the job is.

property failed: bool

Whether this job reached a failing conclusion.

property running: bool

Whether this job has yet to reach a terminal state.

property skipped: bool

Whether this job never ran: its if:, or a job it needs, ruled it out.

property cancelled: bool

Whether this job was stopped before it finished.

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 cancelled_required: tuple[JobStatus, ...]

Required jobs stopped before they finished.

property cancelled: bool

Whether the run was stopped before its required jobs all finished.

A newer push to the same concurrency group does that to the run of the commit under it. A run stopped before GitHub created any job counts too.

property skipped: bool

Whether the run ran no job at all.

A job-level if: ruled every one of them out, which GitHub concludes as a skipped run.

property inconclusive: bool

Whether the run settled without a result for its commit.

Neither red nor green: this workflow says nothing about the tree the run was created for, so nothing here holds up a merge and nothing clears it.

property verdict: str

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

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

Bases: object

Every monitored workflow’s latest run on a branch.

branch: str

Branch the runs were read from.

tip_sha: str = ''

Commit at the tip of the branch when the runs were read.

Empty when the branch history could not be read.

runs: list[RunStatus]

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

skipped_on_tip: list[str]

Workflow filenames that ran no job on the tip.

Only the ones read on an earlier commit: the row of each says nothing about the tip. A workflow with no other run in the window keeps its tip run, whose verdict says the same.

property blocking: list[RunStatus]

Runs holding up a merge.

property runs_on_tip: list[RunStatus]

Runs created for the tip commit itself.

Empty right after a push, while GitHub has yet to create the tip’s runs: every run listed then belongs to an earlier commit.

property settled: bool

Whether every run reached a terminal state.

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

Every workflow in the directory that has runs of its own.

monitored_workflows() narrows this to the ones a push starts, which is the right default for a status report and the wrong set to accept: a schedule-only or dispatch-only workflow has runs worth reading too. A reusable workflow is excluded for the reason it is there, since its jobs only ever appear under a caller’s run.

Parameters:

workflow_dir (Path) – Directory holding the workflow files.

Return type:

tuple[str, ...]

Returns:

Workflow filenames, sorted.

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.

Lists the workflow’s runs through workflow_runs(), which sends a created filter to keep a stale snapshot out. read_ci_status() asks it only about a workflow with no run on the newest commits.

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 in the listing window. 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.

Anchored on the branch tip: reads the runs of each of the newest commits, tip first, and stops as soon as every workflow has one. A workflow whose paths: filter skipped the tip is found on the commit that last started it. Only a workflow with no run in the whole window falls back to latest_run().

A run that ran no job counts as no run at all for that walk, so a workflow that skipped every job on the tip is also read on an earlier commit. A cancelled run stops the walk instead: the runs under it are older than the commits it left without a result, and a green one among them would hide that. A workflow with nothing but skipped runs in the window is reported on the newest of them.

Warning

The branch-wide run listing can answer with a stale snapshot: runs from commits months old, presented as the latest ones. Reading runs commit by commit, from a commit list that comes from git data rather than from the run index, keeps that snapshot out. The fallback listing sends a created filter for the same reason.

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

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

Return type:

CIStatus

Returns:

The collected status.