Skip to content

feat(promotion-gate): say when a promotion owes the admin-read contexts stage, and when to run it - #2536

Merged
modelmirror merged 2 commits into
stagingfrom
feat/contexts-on-demand
Oct 18, 2026
Merged

modelmirror merged 2 commits into
stagingfrom
feat/contexts-on-demand

Conversation

@modelmirror-agent

Copy link
Copy Markdown
Contributor

What and why

scripts/promotion-gate.sh contexts reads repository settings (the rulesets, the environments, the contexts main requires) and judges them against what the code expects. It needs repository-administration read, which no CI token holds, so it was never part of all. Nothing said which promotions needed it, though, and docs/security.md and several other docs still said it ran "unconditionally on every promotion". This is part B of #2502: contexts stays manual and on demand, and each promotion now says whether it owes it.

What's added

  • fedcourts contexts-due (src/fedcourtsai/contexts_due.py).
    • Due when the batch (three-dot diff over main) changes anything the stage judges settings against:
      • .github/workflows/: the required jobs, and every environment a job names, which GitHub creates unprotected;
      • scripts/promotion-gate.sh;
      • required_checks.py, ruleset_checks.py, environment_checks.py;
      • the corpus-check approver: config/corpus-check.yaml and corpus_check.py, which parses it.
    • It says when to run it. The stage unpacks main's own workflows, so a batch's workflow change is checked only after the promotion merges; everything else it reads from the checkout, so before merging. A clean run before the merge is therefore no evidence about the batch's workflows, and the verdict says so.
    • It is advisory. It exits 0 either way, and 1 only if git can't read the diff.
  • promotion-gate.sh all ends with a contexts-due stage. It prints the verdict and never sets fail: a missing origin/main or an unreadable diff is a warning. ci.yml's required promotion-gate check therefore shows the verdict on every promotion PR without being able to go red on it.
  • The promotion train's body (promotion-prep.yml → promotion_body.py) gets one line, kept apart from the gate checks:
    - contexts (admin read, run by hand): **due** — the batch changes `.github/workflows/ci.yml`, `scripts/promotion-gate.sh`.
      Run `scripts/promotion-gate.sh contexts` with an admin-read token before merging.
      It reads `main`'s workflows, so for the workflow changes run `scripts/promotion-gate.sh contexts` again once this merges.
    
    When the read fails, the line is left out (contexts_due: null), and the CLI's error goes to the run log.
  • The docs describe when to run it, and no longer say "every promotion": docs/security.md (four places), docs/pipeline.md (two, plus a new bullet and step 5), docs/corpus-check.md (two, including a "the gate refuses until…" that no automatic gate does) and docs/testing.md.
  • Ownership: contexts_due.py and its test are added to CODEOWNERS (beside the other contexts judgments) and to AGENTS.md's promotion-gate owned list.

Produced example

Run against real history:

Checks

  • Tests:
    • tests/test_contexts_due.py: the path list (every entry exists; near-misses and review-hold.yaml don't trigger); the before/after split; a three-dot diff on a throwaway repo, showing that what main gained after the batch branched isn't the batch's; the CLI's verdict, --json and exit codes; and that the script stage never touches fail and warns without origin/main.
    • tests/test_promotion_body.py: the line appears once on both the staging and the promote/ candidate headers, and says before and/or after.
  • One guard tightened, in tests/test_workflow_promote.py::test_the_contexts_stage_stays_out_of_the_automated_gates. It asserted that the substring "contexts" was absent from all, which the new contexts_due call trips. It now matches the contexts stage as a whole word: it still catches contexts, contexts "$2" and contexts;, and lets contexts_due through. Its docstring says why.
  • Results:
    • scripts/gate.sh lint types docs-tests: green.
    • 376 tests across the promotion, contexts, environment and ruleset suites pass.
    • actionlint 1.7.12 is clean on promotion-prep.yml and ci.yml, and zizmor@1.26.1 --offline has no findings. The online audits can't reach the network from this sandbox.

Reviews

  • code-reviewer: two blockers, both fixed.
    • The candidate body rendered the line twice. The test now renders a promote/ head and counts it.
    • "Run before merging" was wrong for workflow paths, since the stage reads main's workflows. The verdict now splits before and after.
    • Also applied:
      • review-hold.yaml dropped (nothing the stage reads) and corpus_check.py added;
      • the CODEOWNERS entries placed with the sibling judgments;
      • a stricter fail check;
      • the criss-cross merge-base edge noted in the docstring.
    • It agreed that the cli.py wiring of the assert-* commands should stay off the list: it's pinned by tests, and listing it would make nearly every batch due.
  • docs-reviewer: one blocker (the same doubled line), fixed, plus all 11 recommended changes:
    • every "every promotion" / "unconditionally" / "the gate refuses until" that no longer holds;
    • the misplaced colon in security.md;
    • the "Two gates" bullet and step 5;
    • the gate script's tool-needs paragraph.
  • workflow-reviewer: no blockers.
    • It confirmed that producer and consumer are in the same job, that target_sha is set on every path, that origin/main has full history, the || … behaviour under set -e, and jq's handling of a list vs null.
    • Applied its nits: the CLI's stderr goes to contexts-due.log, printed on failure rather than discarded, and ci.yml's step name mentions the verdict.

Merge

This changes the promotion gate (scripts/promotion-gate.sh, ci.yml's gate step name, promotion-prep.yml, tests/test_workflow_promote.py) and AGENTS.md / CODEOWNERS, so it's left for the maintainer.

Refs #2502: part A (sync → test → prepare) shipped with #2519's promotion train, and this is part B.

Effect check (after promotion)

  • The next promotion PR's promotion-gate job log ends with a contexts: due — … or contexts: not due — … line, and the job's conclusion is unchanged by it.
  • The train's generated block carries the contexts (admin read, run by hand) line exactly once.

To check:

gh run view <promotion-gate run> --log | grep 'contexts:'

Generated by Claude Code

…ts stage

`contexts` needs an admin-read token no CI job holds, so it runs by hand. A
new `fedcourts contexts-due` names the paths in a batch that change what the
stage judges settings against (workflows, the gate script, the required-check,
ruleset and environment judgments and the configs they read). The gate's `all`
ends with the verdict, never failing on it, and the promotion train puts it in
the PR's generated block as due or not due.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NtSiNeMSRGjEKSx7yTfdu
contexts reads main's own workflows, so a batch's workflow change is checked
once it merges; what it reads from the checkout is checked before. The verdict
and the promotion body now say which. Drops review-hold.yaml (nothing the stage
reads) and adds corpus_check.py (it parses the approver). Fixes the doubled
line on the train's candidate body, and every doc that still said contexts
runs on every promotion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NtSiNeMSRGjEKSx7yTfdu
@modelmirror
modelmirror enabled auto-merge October 18, 2026 13:52
@modelmirror
modelmirror merged commit e20532d into staging Oct 18, 2026
9 checks passed
@modelmirror
modelmirror deleted the feat/contexts-on-demand branch October 18, 2026 13:54
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.

3 participants