Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/selftests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,63 @@ jobs:
# MUTATION_TARGETS it satisfied `selftests-cover` -- which asks make what
# that list would run -- while never executing here. Covered on paper,
# unrun in fact (Bugbot, .github#300). `make mutations` is the list.
# THE INVENTORY'S CITATIONS, in the SAME already-required context
# (backend#2449).
#
# `repo-inventory.yml` exemption reasons cite tickets, and nothing read
# them: `caller-drift.py` enforces staleness on the ENTRY and never on the
# sentence explaining it. So a reason could say "sequenced behind
# backend#1408" for the eighteen days after that issue closed, with the
# audit green throughout. `make reason-citations` fails when a cited issue
# is closed, or a cited PR was closed without merging.
#
# HERE RATHER THAN A NEW JOB OR A NEW WORKFLOW, for the two reasons the
# steps above were: `selftests` is ALREADY a required context on
# develop/staging/main, so this arms the check with no branch-protection
# edit, and a new context would sit unrequired until somebody armed it --
# which is how a guard ends up advisory. A new REUSABLE would also need its
# own repo-inventory.yml row in every repo, serialising against every other
# guarded-file PR in this single-writer repo.
#
# BOTH HALVES RUN, exactly as for mint-scope: `make selftests` runs the
# FIXTURE suite, which proves the rule catches, and this proves the live
# inventory complies. Either can pass while the other fails.
#
# THE TOKEN IS ORG-WIDE ON PURPOSE, which is the opposite of the narrowing
# `add-to-kanban.yml` argues for and for a stated reason: the reasons in
# repo-inventory.yml cite tickets in `backend`, `client`, `client-runtime`,
# `model-zoo`, `data-ingestors`, `release-train` and `.github`. A token
# scoped with `repositories:` to this repo would resolve NONE of those, and
# the check would report "cannot tell" on almost every citation. Read-only
# in both scopes, and it writes nothing.
#
# FAIL CLOSED ON A MISSING SECRET, which also means this step fails on a
# PR from a FORK, where GitHub withholds secrets. That is the correct
# answer for a required check whose whole contract is that it never reports
# clean from a read it could not make -- and `add-to-kanban.yml` already
# carries the same property in this repo.
- name: Mint a read-only token for the citation check
id: citation-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.RELEASE_TRAIN_APP_ID }}
private-key: ${{ secrets.RELEASE_TRAIN_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
permission-issues: read
permission-pull-requests: read

- name: reason-citations (the live inventory, not fixtures)
env:
GH_TOKEN: ${{ steps.citation-token.outputs.token }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::the citation token is empty. Without it every citation" \
"reads as unresolvable, and this check must not report clean from" \
"a read it could not make." >&2
exit 1
fi
make reason-citations

- name: mutation-check (every runner in MUTATION_TARGETS)
run: make mutations
45 changes: 43 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
# bricked-prs-selftest.yml -> `selftest-bricked-prs`
# kanban-columns.yml -> `selftest-kanban-columns`
# kanban-deploy-state-selftest.yml -> `selftest-kanban-deploy-state`
# selftests.yml -> `selftests`, `mint-scope` AND `selftest-house-rules`
# selftests.yml -> `selftests`, `mint-scope`, `reason-citations`
# AND `selftest-house-rules`
# (the `selftests` required context runs all three. `selftests` + the fixture
# suites prove the rules CATCH; `mint-scope` proves the real workflows COMPLY.
# They disagree in either direction, so neither substitutes for the other.
Expand Down Expand Up @@ -105,6 +106,7 @@ help:
@echo " selftests all $(words $(SELFTEST_FILES)) gate selftests (+ the coverage assertion)"
@echo " credential-scan gitleaks over the whole history, as code-quality.yml runs it"
@echo " audit caller-drift.py against the live org — needs a token"
@echo " reason-citations the live inventory's ticket citations — needs a token"
@echo
@echo " Not reproducible locally, by construction:"
@echo " conformance-gate.yml polls the API for caller-drift's verdict on a"
Expand Down Expand Up @@ -213,6 +215,25 @@ selftest-mint-scope: guard-pyyaml
mint-scope: guard-pyyaml
$(PYTHON) scripts/mint-scope.py

# reason-citations: a ticket cited by a repo-inventory exemption reason must still
# be live (backend#2449). THREE targets, in three different tiers, because they
# answer three different questions:
#
# selftest-reason-citations does the rule CATCH? fixtures, offline, in `check`
# mutation-reason-citations would the suite NOTICE? `mutations`, in `check-all`
# reason-citations does the INVENTORY comply? the live file, needs a token
#
# The third is NOT in `check` or `lint`, unlike `mint-scope` -- and that is the
# only reason the two are wired differently. mint-scope reads workflow files off
# disk; this one reads issue state from the API, so it needs `gh` authenticated
# and the network. `make check` is the offline pre-push tier with an ~18 s budget,
# and a target that can fail on somebody's train wifi does not belong in it. It
# runs in CI from selftests.yml, where the App token is minted, and on demand
# here -- the same split `audit` gets, for the same reason.
.PHONY: reason-citations
reason-citations: guard-pyyaml
$(PYTHON) scripts/reason-citations.py


action-pins:
@set -e; \
Expand Down Expand Up @@ -276,7 +297,7 @@ SELFTEST_FILES := $(sort $(wildcard scripts/tests/*-selftest.py scripts/tests/*-
MUTATION_FILES := $(sort $(wildcard scripts/tests/*-mutations.py))
MUTATION_TARGETS := mutation-house-rules mutation-pipefail-early-close \
mutation-bugbot-gate mutation-closing-ref-gate mutation-bug-to-ready \
mutation-branch-owner
mutation-branch-owner mutation-reason-citations

# THE WHOLE MUTATION TIER, BY NAME OF THE LIST. Every entry point -- CI,
# `check-all`, `lint` -- depends on one of these two rather than on any
Expand Down Expand Up @@ -306,6 +327,7 @@ SELFTEST_TARGETS := selftest-caller-drift selftest-blocked-marker selftest-stand
selftest-version-bump-gate selftest-bricked-prs selftest-kanban-columns \
selftest-kanban-deploy-state selftest-git-reap \
selftest-mint-scope selftest-house-rules \
selftest-reason-citations \
selftest-pipefail-early-close \
selftest-bugbot-gate \
selftest-closing-ref-gate \
Expand Down Expand Up @@ -512,6 +534,25 @@ mutation-closing-ref-gate:

mutation-closing-ref-gate-dry:
$(PYTHON) scripts/tests/closing-ref-gate-mutations.py --dry

# The reason-citation check (backend#2449). guard-pyyaml: it parses
# repo-inventory.yml. The suite stubs `gh` on PATH, so neither of these two needs
# a token or the network -- only the `reason-citations` target above does.
.PHONY: selftest-reason-citations
selftest-reason-citations: guard-pyyaml
$(PYTHON) scripts/tests/reason-citations-selftest.py

# Measured on a laptop: the suite alone ~4 s (every case runs the checker as a
# subprocess against a fixture inventory), the full mutation pass ~95 s for 23
# mutations. Same split as every other runner -- the full pass rides the required
# `selftests` context via `make mutations`, and `--dry` (anchor resolution only,
# milliseconds) rides `make check`.
.PHONY: mutation-reason-citations mutation-reason-citations-dry
mutation-reason-citations:
$(PYTHON) scripts/tests/reason-citations-mutations.py

mutation-reason-citations-dry:
$(PYTHON) scripts/tests/reason-citations-mutations.py --dry
# The bug-label promotion (backend#2348). guard-pyyaml: the suite parses THREE
# workflows -- it extracts the decision out of `customer-priority-bump.yml` by its
# `# selftest:` markers, asserts `col_index` byte-identical to the router's, and
Expand Down
Loading
Loading