ci: run mirror verification on the self-hosted runners - #11
ci: run mirror verification on the self-hosted runners#11thedancingdeveloper wants to merge 1 commit into
Conversation
publish.yml triggers only on push to main and workflow_dispatch, so it never builds unreviewed fork code. The fork-PR argument recorded at the top of ci.yaml is specific to pull_request and does not reach this file, which means there was never a reason for this job to be hosted. ci.yaml is deliberately left alone and is covered by a time-bounded entry in github-policy/runner-exceptions.json instead: it does build fork PRs, and the self-hosted pool is persistent and runs as root inside the tailnet. `verify` is not one of main's required status checks (those are install, test 3.11-3.14 and full-stack e2e), so this cannot make main unmergeable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Closing: this conflicts with a tested invariant in this repository, and that invariant is the better-reasoned rule.
# Only a tag-gated workflow may take the trusted self-hosted pool. Everything
# else is reachable from a fork's pull request.
SELF_HOSTED_WORKFLOWS = frozenset({"release-images.yml", "release-pypi.yml"})and My reasoning in the PR body was narrowly correct — a push to The org-side resolution is to widen the exception instead: |
cadastre enforces a stricter rule than this one, in its own test suite, and that rule is the better-reasoned of the two. tests/test_release_workflow.py pins SELF_HOSTED_WORKFLOWS to the two tag-gated release workflows and asserts every other job is not self-hosted; test_self_hosted_workflows_are_all_tag_gated then asserts everything on that allowlist is reachable only from refs/tags/v*. So the line there is drawn at tag-gated, not at not-fork-reachable. TheDancingDeveloper-org/cadastre#11 tried to move publish.yml on the grounds that push-to-main needs write access and therefore has no fork exposure. That is true and beside the point: the invariant deliberately keeps the self-hosted surface as small as possible. Its own test matrix caught the change, and the PR is closed. Recording it as an exception is the honest outcome -- cadastre's hosted usage is a deliberate, tested security posture, not drift, and the expiry keeps it under review. Verified: all 51 organization repositories now pass, with FarmEggs#2 applied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems, one cause. The gate carried an inlined copy of the checker because a private policy repository cannot be checked out by a public caller, and the exceptions file was consumed by nothing but an expiry loop -- so `runner-exceptions.json` could record an exception but never grant one. The documented workaround was to drop the required status check on the repository entirely, which trades a narrow, expiring, reviewable exception for no gate at all. Making this repository public removes the constraint. scripts/runner_policy.py is now the only copy of the rule; the reusable gate checks this repository out alongside the caller and runs it. Exceptions are keyed by repo and workflow file and may be narrowed to named jobs, which matters immediately: FarmEggs' ci.yml holds two compliant jobs and two that cannot move, and a file-wide exception there would also hide a future regression in the compliant pair. An expired entry still fails the gate and suppresses nothing. Three entries recorded, all with a reason and an expiry: cadastre/ci.yaml -- builds fork PRs on a public repo FarmEggs/ci.yml mobile -- needs a JDK and the Android SDK FarmEggs/ci.yml web -- needs google-chrome and npm Verified against all 50 organization repositories: with these entries applied, the only failures are the jobs that TheDancingDeveloper-org/FarmEggs#2 and TheDancingDeveloper-org/cadastre#11 move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cadastre enforces a stricter rule than this one, in its own test suite, and that rule is the better-reasoned of the two. tests/test_release_workflow.py pins SELF_HOSTED_WORKFLOWS to the two tag-gated release workflows and asserts every other job is not self-hosted; test_self_hosted_workflows_are_all_tag_gated then asserts everything on that allowlist is reachable only from refs/tags/v*. So the line there is drawn at tag-gated, not at not-fork-reachable. TheDancingDeveloper-org/cadastre#11 tried to move publish.yml on the grounds that push-to-main needs write access and therefore has no fork exposure. That is true and beside the point: the invariant deliberately keeps the self-hosted surface as small as possible. Its own test matrix caught the change, and the PR is closed. Recording it as an exception is the honest outcome -- cadastre's hosted usage is a deliberate, tested security posture, not drift, and the expiry keeps it under review. Verified: all 51 organization repositories now pass, with FarmEggs#2 applied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* policy: one checker, and exceptions that actually suppress Two problems, one cause. The gate carried an inlined copy of the checker because a private policy repository cannot be checked out by a public caller, and the exceptions file was consumed by nothing but an expiry loop -- so `runner-exceptions.json` could record an exception but never grant one. The documented workaround was to drop the required status check on the repository entirely, which trades a narrow, expiring, reviewable exception for no gate at all. Making this repository public removes the constraint. scripts/runner_policy.py is now the only copy of the rule; the reusable gate checks this repository out alongside the caller and runs it. Exceptions are keyed by repo and workflow file and may be narrowed to named jobs, which matters immediately: FarmEggs' ci.yml holds two compliant jobs and two that cannot move, and a file-wide exception there would also hide a future regression in the compliant pair. An expired entry still fails the gate and suppresses nothing. Three entries recorded, all with a reason and an expiry: cadastre/ci.yaml -- builds fork PRs on a public repo FarmEggs/ci.yml mobile -- needs a JDK and the Android SDK FarmEggs/ci.yml web -- needs google-chrome and npm Verified against all 50 organization repositories: with these entries applied, the only failures are the jobs that TheDancingDeveloper-org/FarmEggs#2 and TheDancingDeveloper-org/cadastre#11 move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * policy: except cadastre/publish.yml too cadastre enforces a stricter rule than this one, in its own test suite, and that rule is the better-reasoned of the two. tests/test_release_workflow.py pins SELF_HOSTED_WORKFLOWS to the two tag-gated release workflows and asserts every other job is not self-hosted; test_self_hosted_workflows_are_all_tag_gated then asserts everything on that allowlist is reachable only from refs/tags/v*. So the line there is drawn at tag-gated, not at not-fork-reachable. TheDancingDeveloper-org/cadastre#11 tried to move publish.yml on the grounds that push-to-main needs write access and therefore has no fork exposure. That is true and beside the point: the invariant deliberately keeps the self-hosted surface as small as possible. Its own test matrix caught the change, and the PR is closed. Recording it as an exception is the honest outcome -- cadastre's hosted usage is a deliberate, tested security posture, not drift, and the expiry keeps it under review. Verified: all 51 organization repositories now pass, with FarmEggs#2 applied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
cadastre spent 326 GitHub-hosted minutes in August, second only to FarmEggs, in breach of
docs/CI-RUNNER-GATES.md. This moves the part that can move and formally excepts the part that should not.What changes
publish.yml/verify→[self-hosted, node-b, linux, x64].That workflow triggers only on
push: branches: [main]andworkflow_dispatch. It never builds unreviewed fork code, so the security argument written at the top ofci.yamldoes not reach it. It is pureuv+ Python —astral-sh/setup-uvbrings its own interpreter, so it needs nothing the runner image lacks.What deliberately does not change
ci.yamlstays onubuntu-latest. Its header already states the reason, and it is correct:The self-hosted workers are persistent, run as root inside their container, and sit on the tailnet with reachable Forgejo, Infisical, Komodo and registry endpoints. Org fork-PR approval is
first_time_contributors, so a returning contributor's PR would execute without approval (gap 12 inops/docs/github-actions-runner-gaps.md).Instead of a silent breach, it is now an owner-approved, time-bounded entry in
github-policy/runner-exceptions.jsonthat the daily expiry check will surface.Safety
verifyis not a required status check onmain— those areinstall,test (3.11)–test (3.14)andfull-stack e2e, all inci.yamland all untouched. The workflow nameMirror verification (main)is preserved, per the warning in its own header.🤖 Generated with Claude Code