ci: fix MODULE.bazel.lock gate and regenerate the lock file - #11094
Conversation
The pull request lock file check compared MODULE.bazel against itself instead of MODULE.bazel.lock, so it has been blind since it was rewritten in The-OpenROAD-Project#10690. Compare MODULE.bazel.lock, and emit a GitHub error annotation with the exact regeneration command so the diff is actionable. Regenerate MODULE.bazel.lock, which drifted once the rules_python pip extension was invalidated and re-queried pypi.org/simple, adding dist_hashes facts for markdown-it-py, pygments, rich and setuptools. Drop the lock file update from the master push auto-format job: master is protected, so its push is rejected with GH006 whenever the job produces a diff, which is why every master push has been red since 2026-08-04. Signed-off-by: Joao Luis Sombrio <sombrio@sombrasoft.dev>
There was a problem hiding this comment.
Code Review
This pull request updates the MODULE.bazel.lock file to add new versions and hashes for several Python packages, including markdown-it-py (4.2.0), pygments (2.20.0), rich (15.0.0), and setuptools (83.0.0). There are no review comments, and I have no feedback to provide.
80ee8fc to
cbf57c7
Compare
|
FYI @maliberty @hzeller |
|
@maliberty @hzeller
The source of truth for the review is Instead of checking the user's lockfile, CI should ignore |
|
@oharboe Your point 2 holds up. I tested it against the gate in this PR by tampering with
And the tampered spec is genuinely honoured: Bazel fetched the attacker's URL. A module extension is only re-run when its inputs change (usages, bzl transitive digest, recorded files and env). Edit only the recorded results and every digest still validates, so nothing is recomputed, and the check sees no diff. So the split is: the registry portion of the lock is tamper-evident, because recorded hashes are verified against what BCR actually serves. Extension-generated repo specs and pip Two qualifications:
A smaller step toward what you're describingRather than generating the lock server-side at merge, the PR's lock can simply be denied any trust during the check — seed the resolution from the base branch's lock instead: - name: Regenerate from the trusted lock file
run: |
git fetch --depth=1 origin ${{ github.base_ref }}
git checkout FETCH_HEAD -- MODULE.bazel.lock
bazelisk mod deps --lockfile_mode=update
- name: Compare against the lock file in the pull request
run: |
if ! git diff --exit-code ${{ github.sha }} -- MODULE.bazel.lock ; then
echo "::error file=MODULE.bazel.lock::MODULE.bazel.lock does not match the \
resolution performed by CI. Regenerate with 'bazelisk mod deps \
--lockfile_mode=update' and commit the result."
exit 1
fiEntries supplied by the pull request are then never reused: where the PR leaves extension inputs alone, the base branch's entries are reused and any tampering shows up as a diff; where the PR does change them, they are re-resolved on OpenROAD CI. That closes all three rows in the table above while keeping the ordinary regenerate-and-commit workflow, and it needs no PR ownership transfer or write-back. The cost is more spurious failures: resolving from the base lock can pick up PyPI index drift the contributor never saw locally, and they will have to regenerate again to clear it. Worth it, in my view, but it is a real tax. Happy to fold this into this PR, or to keep this one as the pure fix — master has been red since 2026-08-04 — and follow up separately. Preference? |
|
Personally, I have no strong preference on how this specific PR is handled—I am able to do my work either way. My main concern is the long-term security of the project, the quality of life for contributors and reviewers, and not least, those who provide the CI infrastructure for us (which is like unappreciated household chores that are only noticed when they aren't done). While resolving from the base lockfile and comparing it to the PR's lockfile definitely closes the security gap, it preserves the "regenerate and commit" workflow. I believe that is exactly the workflow we should be abandoning. Subjecting contributors to a "tax" of spurious CI failures simply because the PyPI index drifted between their local generation and the CI run is a tax we don't need to levy. It is essentially hipster programming—forcing contributors to perform a manual, awkward dance to satisfy the linter for no discernible difference in the final result. My next pet project is to advocate for shifting all of this busywork to automation that runs after the maintainers take delivery of the PR, before it goes into Maintainers should be able to review the human intent ( This model is safer (security artifacts are generated strictly in a trusted environment), and it massively improves the quality of life for everyone involved. We shouldn't enforce a workflow that causes developers pain just to arrive at the exact same deterministic output our CI infrastructure could have securely generated for them automatically. |
|
Another reason to move to a model where automation takes over after the maintainers approve the human intent is to avoid the "AI nit war" and fuzzing input convergence issue. If I submit a PR, get feedback containing AI-generated nits, fix them locally, and ask for another review, the AI review is run again. Because LLM responses inherently have some variance (fuzzing), and often have a maximum depth of nits they will report per iteration, I just get a new batch of nits. What's worse, the maintainer might be using a different AI reviewer than I am using locally. Humans then have to sit in the middle and manually evaluate if this cycle of nitpicking is actually converging, and each individual nit has to be reviewed for validity. I don't think this teaches the PR submitter or the reviewer much of anything. I would much prefer a workflow where the reviewer uses their AI to grind down enough nits that they are satisfied the core logic is sound and safe to go into The contributor can then simply look at what automated nitpicking was applied on top of their PR after the fact, rather than being forced to act as a human middleman typing out mechanically generated fixes just to satisfy a bot before the merge. |
|
@oharboe You've convinced me, and I'd like to withdraw the trusted-seed suggestion. Checking how the drift actually propagates made the case for you. Bazel only re-queries the PyPI index when a module extension's inputs change, so the lock sits stable until someone touches Your broader point — that this class of work belongs to automation running in a trusted environment rather than to people — is right, and it generalises past the lock file to clang-format, black, buildifier and tclfmt. All of them are convergent, verifiable by re-running, and teach nobody anything when typed out by hand. The one boundary I'd want drawn explicitly when we design it: mechanical, convergent artifacts can safely be generated after approval, because re-running reproduces them and a reviewer loses nothing by not having read them. AI nits are a different animal — they change semantics, they're sometimes wrong, and applying them post-approval means behavioural edits land in I'll merge this PR as it stands, purely to fix the current behaviour — regenerate the lock, compare |
|
Yes, AI nits is a different animal indeed. It does come up here w.r.t. where does the maintainer take delivery of a PR. I'm thinking AI nitpicking fixing is faster and easier for the maintainer to do locally and that it is false economy for everybody to try to get the PR author to do this work based upon a slow and painful feedback loop between three AIs (local AI for PR author, Gemini and local AI for maintainer) and 2+ humans having opinions about the work. A maintainer and his AI will handle this just fine. The PR author can submit a followup PR if some Chesterton Fence was torn down too quickly. |
Every push to
masteron the public repo has failed since 2026-08-04 (example), reported by @gadfortRoot cause
Two independent problems.
1.
MODULE.bazel.lockis stale. Runningbazelisk mod deps --lockfile_mode=updateonmasterproduces a 10 insertion / 2 deletion diff, all underfacts > @@rules_python+//python/extensions:pip.bzl%pip > dist_hashes > https://pypi.org/simple— new entries formarkdown-it-py 4.2.0,pygments 2.20.0,rich 15.0.0andsetuptools 83.0.0.The drift surfaced with #11058 (
bazel: add yamlfix support to fix_lint and lint_test, merged 2026-08-04 21:18 UTC; the run 2.5 h earlier was green and it was the only merge in between). That PR changedbazel/requirements.inandbazel/requirements_lock_3_13.txt, which invalidates therules_pythonpip extension. Re-evaluating it re-queriespypi.org/simplefor the hashless transitive hubs (pgv_pip_deps,grpc_python_dependencies— therequirement file has been generated without hasheswarnings in the CI log) and records the newer releases. The regenerated lock file was never committed.2. The pull request gate never looked at the lock file.
github-actions-check-bazel-lock.ymldid:MODULE.bazelcompared against itself —MODULE.bazel.lockis what--lockfile_mode=updaterewrites. Blind since #10690 (2026-06-19) replaced the workingbazelisk mod deps --lockfile_mode=errorwith this. The original check would have caught #11058.Why master cannot self-heal: the auto-format job on
masterregenerates the lock and pushes, butmasteris protected:So the job is red on every push that produces any diff, and the fix never lands.
Changes
MODULE.bazel.lock.github-actions-check-bazel-lock.yml: compareMODULE.bazel.lock, and on failure emit a::error file=MODULE.bazel.lock::annotation namingbazelisk mod deps --lockfile_mode=updateso the developer sees how to regenerate it.git diff --exit-codealso prints the offending patch.github-actions-on-master-push.yml: drop the lock file regeneration, since its push can never succeed on a protected branch. The lock is now gated on pull requests only.Note
The pip
dist_hashesfacts track the live PyPI index, so any pull request that touchesbazel/requirements*.txtcan pick up unrelated lock churn. The gate now tells the developer exactly what to run, which is the intended workflow.