Repository navigation
ci: deduplicate gate evaluation and verify scanner seeds - #50
Merged
Merged
Conversation
…xtures scripts/pr-gatekeeper.py exceeded the 800-line readiness limit once the workflow-inventory work landed. Move the run inventory, required-workflow evidence and payload collection into scripts/gatekeeper_workflows.py; the aggregator keeps thin facades that inject its _get/_batch seams, so its public API, monkeypatching points and test inventory are unchanged. Stabilise the scanner fixture on Python 3.12: a real venv links bin/python -> python3.12 rather than python3, and the platform stdlib sitecustomize shadows the venv one, so assert the version-aware alias topology and boot the fixture through a site-packages .pth import instead. Resolve the macOS /var -> /private/var temporary-root symlink so the fixture's sys.prefix matches the interpreter's report. Verified: full unittest suite (290 tests) on CPython 3.10 and 3.12, the scanner fixture with and without SCANNER_TEST_UV on both, every scripts/*.py compiled and loaded under 3.10, the enforcing agent-readiness ratchet against origin/main, actionlint 1.16.1, and an unchanged ShellCheck baseline.
scripts/pr-gatekeeper.py imports gatekeeper_workflows at module load, but the reusable gatekeeper fetched only the entrypoint into RUNNER_TEMP. Every run therefore died with ModuleNotFoundError before main() ran, and no `gatekeeper / all-checks-passed` check run was ever posted. Stage both files from the validated job.workflow_sha, remove the task-owned staging outputs first, and require every fetch to succeed with a non-empty file so a stale leftover cannot satisfy staging. The SHA guard, caller contract and the second step's argv are unchanged. Cover the literal stage shell with an offline fixture: a dummy curl serves the committed files, asserts each URL is the same 40-hex revision under seankoji-com/.github/scripts/, and a tripwire proves staging never executes the evaluator. The staged bundle is then loaded with `-I --help` under Python 3.10 and 3.12, and missing/empty/failed fetches, malformed identities and a stale helper are all shown to reject before evaluation.
seankoji
marked this pull request as ready for review
October 13, 2026 06:00
seankoji
enabled auto-merge (squash)
October 13, 2026 06:00
There was a problem hiding this comment.
Changes requested: 3 blocking findings require resolution.
- scripts/scanner_seed.py:
verify()treats the seed directory (os.path.lexists(seed)) as the only "seed present" signal, then callsimmutable(manifest_path). If the directory exists butmanifest.jsonis missing (partially provisioned seed),immutable()'spath.lstat()raises an uncaughtFileNotFoundError;main()only catchesCalledProcessError, soprobe/installabort with a raw traceback instead of the module's controlledValueError. Since the workflow's probe step relies on a clean non-zero exit to decide thecache_mode, make the missing-manifest case an explicitValueError. - scripts/scanner_seed.py:
pins()subscriptsos.environ[name]directly, so any unsetPIN_*raises a bareKeyErrorwith no value/context, inconsistent with theValueErrorused by the rest of the module. This path is reachable via the documented manualbuild/installCLI usage when the caller forgets to export a pin. Report the missing names via the module'sValueError. - scripts/scanner_seed.py: This defensive branch (missing or symlinked
venv/bin/<tool>) has no test:test_no_ambient_scanner_or_venv_is_acceptedfails earlier in thepip checksubprocess,test_system_interpreter_is_rejected_before_any_pipfails in the venv guard, andtest_wrong_scanner_version_fails_after_real_offline_installreaches the later version check. Add a case that pointsinstallat a venv whosebin/<tool>is absent or a symlink and assertsValueErrorand noGITHUB_PATHpublication.
Advisory (P2/P3, or P1 past the round budget). These do not block approval:
- scripts/pr-gatekeeper.py: [P3]
mainaccepts--require-ci, but the--reconcile-openbranch never forwardsargs.require_ciandreconcile_open()has no such parameter, so the flag is silently dropped whenever reconciliation is enabled — which the reusable workflow now always does (args+=(--require-ci)andargs+=(--reconcile-open)in the same step). It is only masked because everyreport()call insidereconcile_openpasses a non-Noneopen_prs, which drives the identicalawaiting_ci = (open_prs is not None or require_ci) and not ci_registeredbranch. The coupling is implicit and fragile: any future edit that stops passingopen_prs(e.g. for the fallback publish) would silently disable merge-commit / empty-inventory enforcement. Forward the flag explicitly. - scripts/pr-gatekeeper.py: [P3] When
required_workflowsis unparseable,entries is Noneforces the destination-resolution block, which (whenPERSONA_REVIEW_REQUIRED=true) runscollect_persona_checks()— one reviews read per matching PR — and the very next statement re-raises the parse error, so those reads are thrown away. Only thepublish_refsresolution needs to precede the error report; skip the discarded review reads.
Resolve all five outstanding findings from review 5499786979: - scanner_seed.pins() names the unset PIN_* variables and raises the module's controlled ValueError instead of a bare KeyError - scanner_seed.verify() treats a seed directory without manifest.json as a controlled ValueError, not a raw FileNotFoundError from lstat, so probe/install exit non-zero without a traceback - tests cover the partially provisioned seed and the absent/symlinked job-local scanner, asserting no GITHUB_PATH entry is published and the install helper never runs or exposes a scanner path on failure - pr-gatekeeper forwards --require-ci through reconcile_open into every report() call, including the read-failure fallback and main() - report() resolves destinations before re-raising a malformed required_workflows manifest and skips the persona review reads whose results the re-raise would discard (still fail closed) - regenerate the four embedded scanner_seed module blobs and their sha256 checksum in reusable-static-analysis.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked issue
Related: #49 — Evaluate each current PR once per serialized gatekeeper refresh, and internal tracking for the parent and the scanner-seed child (
seankoji-com/.github-private#301,seankoji-com/.github-private#302).Written as
Related, notCloses: the charters for this work stay open until hosted CI, org-wide self-hosted runner behaviour and the performance measurements land. Nothing here closes an issue.Summary
Two org-wide reusable-workflow changes, kept in one reviewable branch because the gatekeeper work and the scanner-seed work were developed and validated together:
Both hosted CI failures on the earlier revision are now fixed on this branch: the Python 3.12 scanner-fixture subtests, and the
agent-readinessmonster-filesfinding onscripts/pr-gatekeeper.py.What changed
.github/workflows/reusable-pr-gatekeeper.yml: new optionalrequired_workflowsinput (JSON array of{path, event, ref: head|merge}), default[]so existing callers keep legacy gating. The--reconcile-openrefresh is now folded into the single evaluator invocation whenreconcile_openis set and the run is not a seed, so one process owns destination tracking. The standalone "Refresh all open PR gates" step is removed.scripts/pr-gatekeeper.py: one pass collects the open-PR inventory and resolves each PR's event and current destinations (collect,_collect_for_ref,resolve_review_heads,reconcile_open). Addedparse_required_workflows,required_workflow_checksand_required_run_matchesto bind a declared workflow path/event to the Actions RESThead_shaforheadormerge, including fork and shared-branch disambiguation;_attempt_jobsmakes a rerun that started while jobs were being read ineligible for success. The workflow-run inventory, required-workflow evidence and three-payload collection now live inscripts/gatekeeper_workflows.pybehind thin facades that inject this module's_get/_batchseams, so the public API, monkeypatching points and legacy gatekeeping counts are unchanged.scripts/gatekeeper_workflows.py(new): the extracted workflow-run inventory (collect_workflow_checks), caller-declared required-workflow evidence (parse_required_workflows,required_workflow_checks,_required_run_matches,_attempt_jobs), and the three-payload collection (collect_for_ref,open_pr_ref). No third-party imports and no network of its own;get/batch_fnare injected by the aggregator, which keeps ownership of retry, authentication and monkeypatching..github/workflows/reusable-static-analysis.yml: new "Probe immutable scanner seed" step in the semgrep and zizmor jobs. Install runs only after the probe succeeds, reads the seed from/opt/ci-seeds/scannerswhen the pin-matched seed is verified, otherwise installs from an explicitly probed fallback. Cache key is versioned to v2 and namespaces the trust boundary (runner.environment/trusted-default); cache restore only happens on the fallback path and the cache save is restricted to a successful default-branch push. Semgrep is invoked by absolute venv path.scripts/scanner_seed.py(new): self-containedprobeandinstallhelper with no third-party imports. It verifies the seed tree (pins, wheel hashes, METADATA/RECORD consistency, archive integrity, file modes) and the fallback downloads against the same hashes before anything is executed, and reports a cache mode ofoffline,fallbackoroff. The workflow embeds the exact helper bytes and checksum-checks them before execution, so no runtime fetch is needed.tests/test_scanner_seed.py: the fixture is now version-aware. It asserts the native venv alias topology for the running interpreter (bin/python -> python3on 3.10,bin/python -> python3.12on 3.12,bin/python3 -> pythonfor uv) and boots the fixture through a site-packages.pthimport (import _scanner_fixture_boot), which runs under-Iand survives a platform stdlibsitecustomize. The temporary root is resolved so the macOS/var -> /private/varsymlink does not desync the venv's reportedsys.prefix.tests/test_pr_gatekeeper.py,tests/test_gatekeeper_workflows.py,tests/test_scanner_installer.pyextended,tests/test_scanner_seed.pyadded.Test plan
python3 -m unittest discover -s testsis green at both CPython 3.10.20 and 3.12.15:Ran 296 tests,OKon each (the prior 290-test inventory plus 6 new gatekeeper-staging tests; no test removed or skipped).SCANNER_TEST_UV(realuv0.13.0 offline virtualenvs):Ran 17 tests,OKin all four combinations.python -m py_compile scripts/*.pyand loading everyscripts/*.pyon 3.10 — passes at this head, including the newscripts/gatekeeper_workflows.py(6 modules, 0 failures).agent-readinessenforcing ratchet (--ci,enforce: true, merge-base withorigin/mainat77b72c6) reports 0 regressions;scripts/pr-gatekeeper.pyis 751 lines andscripts/gatekeeper_workflows.py307 lines, both under the repo's 800-line limit.actionlint -shellcheck= -pyflakes=(actionlint 1.16.1) passes clean; the ShellCheck baseline is unchanged (no shell or workflow files changed).e378180; the final gate waits for the required Grumpy Engineer review of this head.Notes for reviewer
e378180. No reviewer has submitted a review yet. This PR remains a draft; no manual dispatch, ready-for-review transition or merge was performed.Packaging repair, landed at
e378180: the reusable workflow now stages bothscripts/pr-gatekeeper.pyand its siblingscripts/gatekeeper_workflows.pyfrom the same validatedjob.workflow_sha(the existing^[0-9a-f]{40}$guard is unchanged; no caller head, branch name or PR-controlled ref is ever used). Each fetch must succeed with a non-empty file, and the two task-owned outputs are removed first so a stale leftover cannot satisfy staging. The old defect is committed as a regression test: an entrypoint-only bundle exits non-zero withModuleNotFoundError: No module named 'gatekeeper_workflows', while the two-file bundle loads under both Python 3.10.20 and 3.12.15 viapython3 -I <staged>/pr-gatekeeper.py --help(no token, no API call, no network) and importsgatekeeper_workflowsfrom the staged directory.tests/test_gatekeeper_workflows.pyruns the literal stage shell against an offline fixturecurlthat serves the committed files and asserts the exact 40-hex revision andscripts/path on every URL, covering a malformed SHA, a missing or empty entrypoint/helper, a failed fetch and a stale pre-existing helper. Existing hosted checks run the published evaluator and cannot exercise this path, so this evidence is local only.teston Python 3.12 (fixed). All 20 failures were intest_scanner_seed.SeedTests.test_literal_workflow_offline_fallback_and_corrupt_paths, for both tools and both venv layouts. A real 3.12 venv linksbin/python -> python3.12, notpython3, so the fixture's alias assertion failed and itsexecinterception never applied, cascading into thecache_mode=offline/ return-code-17 assertions. The fixture no longer writes a venvsitecustomize.pyeither: a platform stdlibsitecustomize(macOS Homebrew, or Debian/Ubuntu's/etc/python3.12/sitecustomize.py) shadows it, so the stub now loads through a site-packages.pthimport that runs under-I. Green at 3.10 and 3.12, with and without realuv.agent-readinessmonster-files(fixed).scripts/pr-gatekeeper.pyhad grown from 800 to 1004 lines. The workflow-inventory and required-workflow logic moved toscripts/gatekeeper_workflows.py; the aggregator keeps re-exported facades (WorkflowInventoryDrift,parse_required_workflows,collect_workflow_checks,required_workflow_checks) so imports, monkeypatching and gatekeeping behaviour are unchanged. Both files are now under the limit and the enforcing ratchet scores 0 regressions.runner.environmenttrust separation,pip/wheel install behaviour, actual semgrep/zizmor findings, org-wide caller manifests pinned to these reusable workflows, and any wallclock or cost saving. Those need the hosted runners and are out of scope for a source review.required_workflowsdefault keeps every existing caller on the previous behaviour; a caller only gets the stricter binding when it opts in.AI assistance
litellm/deepseek-v4.1-flashmonster-filesonpr-gatekeeper.py) without changing gatekeeping behaviour. Every local gate above was re-executed at this head.