Skip to content

ci: emit the gate status check this repo is required to produce - #25

Merged
noahgift merged 1 commit into
masterfrom
ci/emit-gate-context
Aug 28, 2026
Merged

ci: emit the gate status check this repo is required to produce#25
noahgift merged 1 commit into
masterfrom
ci/emit-gate-context

Conversation

@noahgift

Copy link
Copy Markdown
Contributor

The defect, measured

The paiml org ruleset "Green Main" (id 13878864) requires exactly one status context:

$ gh api orgs/paiml/rulesets/13878864 \
    --jq '.rules[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context'
gate

This repo emits no such check. Its only two workflows are main.yml — a
schedule / workflow_dispatch README updater that never runs on a PR — and
pr-gate.yml, a pull_request_target authorization gate. Neither produces a
context named gate. The consequence is visible on the repo's open PR #18:

$ gh pr view 18 -R paiml/python_devops_book --json mergeStateStatus,statusCheckRollup
{"mergeStateStatus":"BLOCKED","statusCheckRollup":[]}

BLOCKED with an empty rollup. No check is red — the required one simply
cannot be produced. There is nothing a contributor can fix, and the only way
through is an admin override that the org rules correctly forbid. Eight repos
are in this state right now. A ruleset naming a context no workflow emits does
not raise the bar; it closes the repo.

The fix

Add a job whose name is literally gate. The ruleset matches the context,
not a display name, so a friendlier name would silently re-break merging for
everyone. Triggers are pull_request and push to master (this repo's
default branch), on ubuntu-latest — matching the runner the repo's existing
job already uses.

The gate measures something real

This repo has no build and no test suite; it is the source listing for Python
for DevOps
. There are therefore no existing CI jobs to aggregate, so
ci/gate.py checks what the repo actually is — the examples a reader is
expected to be able to run:

check denominator what it does
python-syntax 47 files ast.parse every tracked .py
notebook-structure 5 files JSON parse + nbformat / cells / cell_type
shell-syntax 8 files bash -n
yaml-parse 51 files yaml.safe_load_all
json-parse 9 files json.loads

Two properties keep this from becoming a green light that means nothing — which
would be worse than the block it replaces:

  1. Every check prints its denominator and fails when it inspected zero
    files.
    A check that silently matches nothing must not read as a pass.
  2. --self-test runs first and feeds every checker an input it must
    reject, failing the job if any checker accepts it. A checker that cannot
    fail is not a check.

Vendored node_modules/ is excluded throughout, and Helm chart templates/
are excluded from the YAML check because they are Go text/template, not YAML.

Verified in both directions before pushing

$ python3 ci/gate.py --self-test
  ok   python-syntax: rejected its broken fixture
  ok   notebook-structure: rejected its broken fixture
  ok   shell-syntax: rejected its broken fixture
  ok   yaml-parse: rejected its broken fixture
  ok   json-parse: rejected its broken fixture
self-test: probed 5 checkers, 0 inert

$ python3 ci/gate.py
  ok   python-syntax: checked 47 files, 0 bad
  ok   notebook-structure: checked 5 files, 0 bad
  ok   shell-syntax: checked 8 files, 0 bad
  ok   yaml-parse: checked 51 files, 0 bad
  ok   json-parse: checked 9 files, 0 bad
gate: ran 5 checks, 0 failed

And with a real defect injected into a real file — def broken(: appended to
src/chap07-Monitoring/web.py — the gate goes red and names it:

  FAIL python-syntax: checked 47 files, 1 bad
       -> src/chap07-Monitoring/web.py: line 37: invalid syntax
gate: ran 5 checks, 1 failed   (exit 1)

Same for truncating ml_regression.ipynb to not json.

Bootstrapping

A separate PR adding gate would itself be blocked for not emitting gate
chicken and egg. It resolves itself: a pull_request workflow added in a PR
runs on that PR, so this PR emits its own gate and can merge normally. No
bypass is used or needed.

Once this merges, PR #18 (and every future PR here) can be re-run and will get
a real gate context instead of an unsatisfiable one.

🤖 Generated with Claude Code

MEASURED DEFECT. The paiml org ruleset "Green Main" (id 13878864) requires
exactly one status context:

    $ gh api orgs/paiml/rulesets/13878864 \
        --jq '.rules[]|select(.type=="required_status_checks")
              |.parameters.required_status_checks[].context'
    gate

This repo emits no such check. Its only workflows are `main.yml` (a
`schedule`/`workflow_dispatch` README updater -- it never runs on a PR) and
`pr-gate.yml` (a `pull_request_target` authorization gate). So:

    $ gh pr view 18 -R paiml/python_devops_book \
        --json mergeStateStatus,statusCheckRollup
    {"mergeStateStatus":"BLOCKED","statusCheckRollup":[]}

BLOCKED with an empty rollup: no check is red, the required one simply cannot
be produced. Nothing a contributor does can clear it, and the only way through
is an admin override the org rules correctly forbid. Eight repos are in this
state. A ruleset naming a context no workflow emits does not raise the bar --
it closes the repo.

FIX. Add a job whose name is literally `gate` (the ruleset matches the CONTEXT,
so a friendlier display name would silently re-break merging), triggered on
`pull_request` and on `push` to `master`, on `ubuntu-latest` to match the
runner this repo's existing job already uses.

The gate measures something real. This repo has no build and no test suite --
it is the source listing for *Python for DevOps* -- so there are no CI jobs to
aggregate, and `ci/gate.py` instead checks what the repo actually IS:

    python-syntax        47 files   ast.parse every tracked .py
    notebook-structure    5 files   JSON + nbformat/cells/cell_type
    shell-syntax          8 files   bash -n
    yaml-parse           51 files   safe_load_all (Helm Go-templates excluded)
    json-parse            9 files   json.loads

Two properties keep it from becoming a green that means nothing:

  * Every check prints its DENOMINATOR and FAILS when it inspected zero files.
    A check that silently matches nothing must not read as a pass.
  * `--self-test` runs first and feeds every checker an input it MUST reject,
    failing the job if any checker accepts it. A checker that cannot fail is
    not a check.

Verified in both directions before pushing. Self-test: 5/5 checkers rejected
their broken fixtures. Clean tree: all 120 tracked files pass. Injected
defects, one per shape: appending `def broken(:` to
src/chap07-Monitoring/web.py and truncating
src/chap14-MLOps/regression-concepts/ml_regression.ipynb to `not json` each
turned the gate red (exit 1), naming the file and the line.

Vendored node_modules/ is excluded throughout; Helm chart templates/ are
excluded from the YAML check because they are Go text/template, not YAML.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@noahgift
noahgift merged commit 494c8a4 into master Aug 28, 2026
2 of 3 checks passed
@noahgift
noahgift deleted the ci/emit-gate-context branch August 28, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant