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.yamlemits✅ ubuntu-26.04 / py3.10while the release engine emitsworkflow / ✅ 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
conclusionissuccesswhile acontinue-on-errorprobe inside it crashed, and itsstatusstill readsqueuedwhile a dozen of its jobs have already finished. Neither answers “is anything broken”.A run that failed around its jobs. A
failureconclusion with no failed job is a workflow-level error: an invalidstrategy.matrixexpression, 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 bundledtests.yamldoes 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 tolatest_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-shipskill 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-statustable.
- class repomatic.github.ci_status.JobStatus(name, status, conclusion)[source]¶
Bases:
objectOne job of one workflow run.
- 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.yamlleads 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.
- class repomatic.github.ci_status.RunStatus(workflow, run_id, head_sha, status, conclusion, jobs=())[source]¶
Bases:
objectThe latest run of one workflow on one branch.
- 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 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 askippedrun.
- class repomatic.github.ci_status.CIStatus(branch, tip_sha='', runs=<factory>, skipped_on_tip=<factory>)[source]¶
Bases:
objectEvery monitored workflow’s latest run on a branch.
- tip_sha: str = ''¶
Commit at the tip of the branch when the runs were read.
Empty when the branch history could not be read.
- 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.
- 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.
- 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.
- 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 acreatedfilter to keep a stale snapshot out.read_ci_status()asks it only about a workflow with no run on the newest commits.- Parameters:
- Return type:
- Returns:
The run, or
Nonewhen 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 tolatest_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
createdfilter for the same reason.