Harden CI: the pip bootstrap step installs from a hash-pinned set - #5929
Merged
Conversation
The jobs that already install everything else with --require-hashes still
opened with a bare `python -m pip install --upgrade pip`. That resolve was
unpinned, so the job fetched whatever pip PyPI served that minute and then
used THAT pip to verify every hash below it. A hash-checked install is only
worth as much as the installer doing the checking, so this was the one
remaining gap in those jobs' supply chain.
Nine call sites across five workflows now install from one of two new
hash-pinned sets:
.github/requirements/pip-bootstrap.txt pip
.github/requirements/pip-build-bootstrap.txt pip + build (+ packaging,
pyproject_hooks)
Deliberately two files. Seven of the ten bootstrap sites in the repo do not
build anything, and three of those bootstrap a throwaway venv that the E2E
suites then use to prove a freshly installed wheel boots on its own. Adding
`build` and its closure to those venvs would widen the dependency surface
the test is measuring, which is exactly the bug an isolated-venv test exists
to catch. Every job's installed set is identical to what it was before.
`--upgrade` is gone because naming an exact version does the same job: pip
26.2.1 is installed over whatever actions/setup-python shipped, which is
what the upgrade was reaching for. Both files join the /.github/requirements
Dependabot entry, so the pins move rather than freeze.
Scope: only the five PR-triggered workflows, so this PR's own CI exercises
every one of the nine hunks. The remaining bootstrap sites are deliberately
left for a separate change -- see the PR body.
No-PRD: CI-only change under .github/; no product behaviour is touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qRmtoZTzuS7CxzZ9GnsU9
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
This was referenced Sep 13, 2026
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.
Product record: No-PRD: CI-only change under
.github/; no product behaviour is touched.Risk: Low, and contained to CI. If a hash or version in either new file is wrong, the affected job fails loudly at its first step with a pip hash-mismatch error — there is no silent-wrong mode, and nothing is published or deployed from these five workflows. Undone by reverting the commit. Nothing in flight collides: no other open PR touches
.github/requirements/. The one behaviour change to reason about is that pip is now installed at an exact version rather than "latest", which is the point of the change.Summary
--require-hashesstill opened with a barepython -m pip install --upgrade pip. That resolve was unpinned, so the job fetched whatever pip PyPI served that minute and then used that pip to verify every hash below it. A hash-checked install is only worth as much as the installer doing the checking.PinnedDependenciesIDfindings, continuing the batches merged in Harden CI: the security scanners install from a hash-pinned set #5889, Harden CI: the dashboard-boot jobs install from a hash-pinned set #5897, Harden CI: the Playwright E2E jobs install from a hash-pinned set #5899, Harden CI: the PyPI publish path installs from a hash-pinned set #5902, Harden CI: the ci.yml Python test jobs install from a hash-pinned set #5903, Harden CI: the gate jobs install from a hash-pinned set #5907 and Harden CI: the live-E2E jobs install from a hash-pinned set #5915.--upgradeis gone because naming an exact version does the same job: pip 26.2.1 is installed over whateveractions/setup-pythonshipped, which is what the upgrade was reaching for anyway.The two files
.github/requirements/pip-bootstrap.txtpip.github/requirements/pip-build-bootstrap.txtpip,build,packaging,pyproject_hooksDeliberately two files rather than one. Six of the nine sites do not build anything, and three of those bootstrap a throwaway venv that the E2E suites then use to prove a freshly installed wheel boots on its own. Adding
buildand its closure to those venvs would widen the dependency surface the test is measuring — which is exactly the bug an isolated-venv test exists to catch. As it stands, every job's installed set is byte-for-byte what it was before this PR, with the versions now fixed.Both files join the existing
/.github/requirementsDependabot entry, so the pins move rather than freeze — the same pin/updater pairing the sibling files document.Scope, and what is deliberately left out
This PR touches only the PR-triggered workflows, so its own CI exercises all nine hunks. The remaining seven bootstrap sites are left for separate changes, for reasons that are not "ran out of time":
conformance-heartbeat.yml(2),desktop-artifacts.yml(2),release-canary.yml(1),release-on-merge.yml(1) — none arepull_request-triggered, so no PR can validate a change to them before it lands onmain. Pinning the release pipeline's bootstrap on a run that cannot test it is how you find out at release time.pr-screenshots.yml(1) — it checks out intohead/andbase/, so there is no.github/at the workspace root and the reference has to name one of the two trees. That is a trust decision (head/is PR-author-controlled), not a path fix, and it deserves its own review. It would also fail on this PR, sincebaseismainand would not yet contain the file.Test plan
python3 -c "import yaml,glob; [yaml.safe_load(open(f)) for f in glob.glob('.github/workflows/*.yml')]"— 40/40 OK--require-hashesin a clean 3.11 venv, and pip ends at 26.2.1 in each — i.e. the replacement does what--upgrade pipdidpython -m build --wheelwith the pinned build toolchain producesclawmetry-*-py3-none-any.whl, so the three build sites still buildcross-repo-handoffinstall order (pip-bootstrap.txtthencross-repo-handoff.txt) in one venv — no version conflict; both pinpackaging==26.3identicallytests/test_workflow_yaml_valid.py,tests/test_action_refs_pinned.py,tests/test_ci_workflow_invocations_are_real.py— 613 passed, 370 skippedmake lint-ci-test-coverage,lint-runtime-count,lint-module-map,lint-daemon-allowlist— all greenmake lint-pyfails identically (231 errors, all indashboard.py) on this branch and on a cleanmain, so it is pre-existing and unrelated; this diff contains no PythonHashes were generated by the procedure the sibling requirements files document:
pip install --dry-run --ignore-installed --reporton Linux / CPython 3.11 to match the jobs, then everysha256underurlsin the PyPI JSON for each resolved version.🤖 Generated with Claude Code
https://claude.ai/code/session_011qRmtoZTzuS7CxzZ9GnsU9
Generated by Claude Code