Skip to content

ci: deduplicate gate evaluation and verify scanner seeds - #50

Merged
seankoji merged 8 commits into
mainfrom
t3/issue-301-publish-public
Oct 13, 2026
Merged

seankoji merged 8 commits into
mainfrom
t3/issue-301-publish-public

Conversation

@seankoji

@seankoji seankoji commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

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, not Closes: 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:

  1. The gatekeeper evaluates each current PR destination once per refresh instead of running the event path and a separate "refresh all open PRs" path in two processes, and it can require declared workflows to be green at a chosen ref.
  2. Static analysis verifies the pinned scanner tooling against a read-only immutable seed before any cache transfer, and falls back to a probed install path only when the seed is absent or unusable.

Both hosted CI failures on the earlier revision are now fixed on this branch: the Python 3.12 scanner-fixture subtests, and the agent-readiness monster-files finding on scripts/pr-gatekeeper.py.

What changed

  • .github/workflows/reusable-pr-gatekeeper.yml: new optional required_workflows input (JSON array of {path, event, ref: head|merge}), default [] so existing callers keep legacy gating. The --reconcile-open refresh is now folded into the single evaluator invocation when reconcile_open is 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). Added parse_required_workflows, required_workflow_checks and _required_run_matches to bind a declared workflow path/event to the Actions REST head_sha for head or merge, including fork and shared-branch disambiguation; _attempt_jobs makes 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 in scripts/gatekeeper_workflows.py behind thin facades that inject this module's _get/_batch seams, 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_fn are 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/scanners when 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-contained probe and install helper 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 of offline, fallback or off. 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 -> python3 on 3.10, bin/python -> python3.12 on 3.12, bin/python3 -> python for uv) and boots the fixture through a site-packages .pth import (import _scanner_fixture_boot), which runs under -I and survives a platform stdlib sitecustomize. The temporary root is resolved so the macOS /var -> /private/var symlink does not desync the venv's reported sys.prefix.
  • Tests: tests/test_pr_gatekeeper.py, tests/test_gatekeeper_workflows.py, tests/test_scanner_installer.py extended, tests/test_scanner_seed.py added.

Test plan

  • Tests pass locally — full python3 -m unittest discover -s tests is green at both CPython 3.10.20 and 3.12.15: Ran 296 tests, OK on each (the prior 290-test inventory plus 6 new gatekeeper-staging tests; no test removed or skipped).
  • Scanner fixture green at both interpreters, with and without SCANNER_TEST_UV (real uv 0.13.0 offline virtualenvs): Ran 17 tests, OK in all four combinations.
  • Repository's other local gate — python -m py_compile scripts/*.py and loading every scripts/*.py on 3.10 — passes at this head, including the new scripts/gatekeeper_workflows.py (6 modules, 0 failures).
  • agent-readiness enforcing ratchet (--ci, enforce: true, merge-base with origin/main at 77b72c6) reports 0 regressions; scripts/pr-gatekeeper.py is 751 lines and scripts/gatekeeper_workflows.py 307 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).
  • No secrets, personal data, private hostnames, or private logs were added.
  • CI passes — tests, static analysis, CodeQL and readiness pass at e378180; the final gate waits for the required Grumpy Engineer review of this head.
  • Verified on live infra where applicable — not claimed, see Notes for reviewer.

Notes for reviewer

  • Remaining gate: the current evaluator check reports that all other checks and commit statuses passed, and it is waiting for required Grumpy Engineer review dispatch at 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 both scripts/pr-gatekeeper.py and its sibling scripts/gatekeeper_workflows.py from the same validated job.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 with ModuleNotFoundError: No module named 'gatekeeper_workflows', while the two-file bundle loads under both Python 3.10.20 and 3.12.15 via python3 -I <staged>/pr-gatekeeper.py --help (no token, no API call, no network) and imports gatekeeper_workflows from the staged directory. tests/test_gatekeeper_workflows.py runs the literal stage shell against an offline fixture curl that serves the committed files and asserts the exact 40-hex revision and scripts/ 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.

  • Hosted test on Python 3.12 (fixed). All 20 failures were in test_scanner_seed.SeedTests.test_literal_workflow_offline_fallback_and_corrupt_paths, for both tools and both venv layouts. A real 3.12 venv links bin/python -> python3.12, not python3, so the fixture's alias assertion failed and its exec interception never applied, cascading into the cache_mode=offline / return-code-17 assertions. The fixture no longer writes a venv sitecustomize.py either: a platform stdlib sitecustomize (macOS Homebrew, or Debian/Ubuntu's /etc/python3.12/sitecustomize.py) shadows it, so the stub now loads through a site-packages .pth import that runs under -I. Green at 3.10 and 3.12, with and without real uv.
  • agent-readiness monster-files (fixed). scripts/pr-gatekeeper.py had grown from 800 to 1004 lines. The workflow-inventory and required-workflow logic moved to scripts/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.
  • Deliberately not claimed: real scanner execution on a Linux self-hosted runner, the runner.environment trust 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.
  • The seed helper is duplicated into the workflow as checksum-pinned embedded bytes by design; a mismatch fails the probe and the job takes the fallback path rather than executing unverified code.
  • The required_workflows default keeps every existing caller on the previous behaviour; a caller only gets the stricter binding when it opts in.
  • The scanner seed tests execute offline venv construction and never reach the network.

AI assistance

  • Tool / harness: OpenCode (T3 Code delegated worker)
  • Model: litellm/deepseek-v4.1-flash
  • Mode: background agent
  • Degree of human review: none yet — this is a draft for human review
  • Anything else worth flagging: the original five commits were replayed verbatim as signed cherry-picks from an already-reviewed local integration branch. A follow-up signed commit on this head fixes the two hosted failures (3.12 scanner-fixture subtests, monster-files on pr-gatekeeper.py) without changing gatekeeping behaviour. Every local gate above was re-executed at this head.

…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
seankoji marked this pull request as ready for review October 13, 2026 06:00
@seankoji
seankoji enabled auto-merge (squash) October 13, 2026 06:00

@bot-grumpy-engineer bot-grumpy-engineer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 calls immutable(manifest_path). If the directory exists but manifest.json is missing (partially provisioned seed), immutable()'s path.lstat() raises an uncaught FileNotFoundError; main() only catches CalledProcessError, so probe/install abort with a raw traceback instead of the module's controlled ValueError. Since the workflow's probe step relies on a clean non-zero exit to decide the cache_mode, make the missing-manifest case an explicit ValueError.
  • scripts/scanner_seed.py: pins() subscripts os.environ[name] directly, so any unset PIN_* raises a bare KeyError with no value/context, inconsistent with the ValueError used by the rest of the module. This path is reachable via the documented manual build/install CLI usage when the caller forgets to export a pin. Report the missing names via the module's ValueError.
  • scripts/scanner_seed.py: This defensive branch (missing or symlinked venv/bin/<tool>) has no test: test_no_ambient_scanner_or_venv_is_accepted fails earlier in the pip check subprocess, test_system_interpreter_is_rejected_before_any_pip fails in the venv guard, and test_wrong_scanner_version_fails_after_real_offline_install reaches the later version check. Add a case that points install at a venv whose bin/<tool> is absent or a symlink and asserts ValueError and no GITHUB_PATH publication.

Advisory (P2/P3, or P1 past the round budget). These do not block approval:

  • scripts/pr-gatekeeper.py: [P3] main accepts --require-ci, but the --reconcile-open branch never forwards args.require_ci and reconcile_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) and args+=(--reconcile-open) in the same step). It is only masked because every report() call inside reconcile_open passes a non-None open_prs, which drives the identical awaiting_ci = (open_prs is not None or require_ci) and not ci_registered branch. The coupling is implicit and fragile: any future edit that stops passing open_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_workflows is unparseable, entries is None forces the destination-resolution block, which (when PERSONA_REVIEW_REQUIRED=true) runs collect_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 the publish_refs resolution needs to precede the error report; skip the discarded review reads.

Comment thread scripts/scanner_seed.py
Comment thread scripts/scanner_seed.py
Comment thread scripts/scanner_seed.py
Comment thread scripts/pr-gatekeeper.py Outdated
Comment thread scripts/pr-gatekeeper.py
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
@seankoji
seankoji merged commit 56324b9 into main Oct 13, 2026
31 checks passed
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