Skip to content

feat: name a required workflow that has no file (the silent never-green defect) #188

Description

@Jammy2211

Overview

config/repos.yaml declares required_workflows per group, and heart/checks/ci_status.py's rollup() scores a repo over exactly those workflows. A required workflow with no runs at all is not scored as a failure — it simply never satisfies all_green, so the repo sits at {"conclusion": "", "status": "in_progress"} forever: able to go red, never able to go green, and never CI-clean to the readiness gate. On the dashboard that is indistinguishable from a run is in flight, which is why the state survives unnoticed.

One workspace repo sat in exactly that state from whenever it joined its group until 2026-08-24, when a human noticed the asymmetry. A survey of all 15 repos across the four groups that gate found it was the only instance, and it has since been fixed — so this is not a bug hunt. It is a guard against the hole reopening, which it does on every edit to config/repos.yaml: adding a repo to a group, or a workflow to a group's required_workflows, silently makes repos un-greenable and produces no red anything.

Plan

  • Add a Heart check that asserts, for every repo in a group with required_workflows, that a workflow exists whose name matches each required entry.
  • Read the workflow list, not the runs payload — it is the only source that separates "no workflow file" from "file exists, has never run on main". The second is genuinely pending and must stay pending.
  • Match on each workflow's name field, never its filename: name is what ci_status matches runs against, so a filename-based check could pass while the roll-up still starves.
  • Classify a missing file as YELLOW and as a configuration finding rather than red CI — the repo's code is fine, its gate is not wired up, and red would misattribute the fault.
  • Degrade honestly: no gh (the web/mobile session) skips the check; a per-repo fetch error is an unknown, never an implied pass.
Detailed implementation plan

Affected Repositories

  • PyAutoHeart (primary, and only)

Branch Survey

Repository Current Branch Dirty?
./PyAutoHeart main clean

Suggested branch: feature/required-workflow-file-drift

Cost — why this is one extra call, not a new tier

The original proposal assumed either a periodic Actions-API sweep or a check that runs where sibling checkouts exist. Reading heart/checks/ci_status.sh says otherwise: it already loops every polled repo in parallel making two cheap gh api metadata calls each (actions/runs?branch=main, commits/main). A third — GET /repos/{owner}/{repo}/actions/workflows — is the same shape and the same cost, and only repos in a group that gates need it (17 today; advisory groups are skipped).

Implementation Steps

  1. heart/checks/required_workflow_drift.py — new module, mirroring manifest_drift.py's shape (own sidecar, available: false when it cannot run, coloured one-line main()):
    • polled_repos(config_path){owner, name, group} from the repos: block.
    • gating_repos(config_path) — those whose group declares required_workflows, with that list; reuses ci_status.load_required_workflows rather than duplicating the mapping.
    • fetch_workflow_names(owner_name)gh api repos/<o>/<n>/actions/workflows?per_page=100 --jq '.workflows[].name', bounded by a timeout, stderr collapsed to one line.
    • check_one(repo){name, group, required, present, missing, error}.
    • run()shutil.which("gh") guard, ThreadPoolExecutor fan-out, writes $HEART_STATE_DIR/required_workflow_drift.json.
  2. heart/state.py — aggregate the sidecar into state.json.
  3. heart/readiness.py — a YELLOW leg per missing workflow (required_workflow_drift), plus a stale reason naming any repo whose workflow list could not be read (required_workflow_unknown) so a missing gate cannot hide behind a 403. Scoring weights for both keys.
  4. heart/tick.sh — run it, || heart_log WARN like its siblings.
  5. tests/test_required_workflow_drift.py + four cases in tests/test_readiness.py.

Key Files

  • heart/checks/required_workflow_drift.py — new; the check
  • heart/readiness.py — the YELLOW leg, the stale leg, _WEIGHTS
  • heart/state.py — sidecar aggregation
  • heart/tick.sh — tick wiring
  • tests/test_required_workflow_drift.py, tests/test_readiness.py — 15 new tests

Testing

Full suite must stay green (python3 -m pytest -q -n auto). The suite is stdlib + PyYAML only and must not reach the network, so every fetch is monkeypatched. The tenant-firewall gate (repos_sync.py --only "tenant firewall (organ code)") also runs on Heart PRs: the new files must name no instance fact, so no allowlist entry is needed.

Out of scope

  • Folding the finding into ci_status's own sidecar or rollup() return. rollup() is the release gate; changing its shape to carry a configuration finding would ripple through readiness, the dashboard and publish for no gain, and risks colouring a missing gate as CI state.
  • The release-ci profile's unobserved list, whose sentence is specifically about dev-box-local evidence. This check is not dev-box-local.

Original Prompt

Click to expand starting prompt

Heart cannot see a required workflow that has no file — the silent never-green defect

Type: feature
Target: pyautoheart
Repos:

  • @PyAutoHeart
    Difficulty: medium
    Autonomy: supervised
    Priority: normal
    Status: formalised
    Filed: 2026-08-27

PyAutoHeart/config/repos.yaml declares required_workflows per group, and
heart/checks/ci_status.py's rollup() scores a repo over exactly those
workflows. A required workflow that has no runs at all is not scored as a
failure — it simply never satisfies all_green, so the repo sits at
{"conclusion": "", "status": "in_progress"} forever: able to go red, never
able to go green, and invisible to the readiness/release gate as CI-clean.

That is indistinguishable, on the dashboard, from "a run is in flight". Nothing
anywhere says "this repo is missing a gate".

This was found the hard way: autocti_workspace sat in that state from whenever
it was added to the workspaces group until 2026-08-24, when a human noticed the
asymmetry (a red Smoke Tests still reported failure correctly, so the repo could
go red but never green). Fixed for that instance by autocti_workspace#30; the
class was left as a proposal, deliberately, so the fix PR was not widened. This
is that proposal, filed.

Why it is worth a check even though the class is currently empty

A survey of all 15 repos across the four groups carrying required_workflows
(run 2026-08-24, on the issue) found autocti_workspace was the only
instance, and #30 removed it. So this is not a bug hunt — it is a guard against
the hole reopening, which it does on every edit to config/repos.yaml:

  • adding a repo to a group silently makes it un-greenable until it carries a file
    for each of that group's required workflows;
  • adding a workflow to a group's required_workflows silently does the same to
    every repo in the group at once.

Both edits look harmless in review. Neither produces a red anything.

Shape

A cheap per-repo assertion: for every repo in a group with required_workflows,
a workflow exists whose parsed name: field matches each required entry. Match
on name:, not the filename — name: is what ci_status matches against, so a
filename-based check could pass while the roll-up still starves.

Two design notes, one of which corrects the original proposal:

  1. It probably needs no new data source, and no sibling checkouts. The
    original proposal assumed either a periodic Actions-API sweep or a check that
    runs where the workspace checkouts exist. But heart/checks/ci_status.sh
    already loops every polled repo in parallel and makes two cheap gh api
    metadata calls each (actions/runs?branch=main, commits/main). A third —
    GET /repos/{owner}/{repo}/actions/workflows, which returns every workflow
    file with its name — is the same shape and the same cost, and it is the
    only call that can tell missing file from file exists, has never run on
    main
    . Check whether that fits the <30 s tick budget before falling back to a
    deep/on-demand tier; ci_status.sh's own header argues the existing two calls
    are cheap enough, so a third plausibly is too.
  2. A missing file is a configuration finding, not red CI. The repo's code is
    fine; its gate is not wired up. Colouring it red misattributes the fault.
    heart/checks/manifest_drift.py is the precedent to mirror — a drift check
    that classifies as YELLOW ("hygiene that will eventually break something, not
    an immediate release blocker"), writes its own sidecar, and is consumed by
    readiness.py as a caution rather than a gate.

The required_workflows block in config/repos.yaml already carries each
group's filenames in a trailing comment (# smoke_tests.yml + navigator_check.yml),
which is a useful cross-check but not the matching key.

Acceptance

  • Adding a repo to a group whose required workflows it does not have produces a
    named finding, not a silent in_progress.
  • The finding distinguishes "no workflow file" from "workflow exists, no runs on
    main HEAD yet" — the second is genuinely pending and must stay pending.
  • A test in PyAutoHeart/tests/ alongside test_manifest_drift.py /
    test_ci_status.py.

Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions