Skip to content

Commit 93aac16

Browse files
feat(bzlmod-lock-check): split into two jobs, add lockfile check, guard against pull_request_target (#122)
- Split single job into bzlmod-tidy-check and bzlmod-lockfile-check so both results are reported independently in the PR checks UI - Add `bazel mod deps --lockfile_mode=error` to verify the committed lockfile matches the resolved dependency graph - Add explicit guard step that fails (not skips) when called from pull_request_target, which would otherwise run Bazel on untrusted fork code with access to repository secrets - Remove unsafe fork checkout (head_ref + fork repository override) in favor of the default checkout, which is safe for pull_request and schedule events - Add header documentation with pre-commit alternative and security note - Update README section 16 to reflect both jobs, the security constraint, and the pre-commit recommendation
1 parent af34772 commit 93aac16

2 files changed

Lines changed: 97 additions & 16 deletions

File tree

.github/workflows/bzlmod-lock-check.yml

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,33 @@
1111
# SPDX-License-Identifier: Apache-2.0
1212
# *******************************************************************************
1313

14+
# Verifies that MODULE.bazel and MODULE.bazel.lock are consistent and up to date.
15+
#
16+
# Two checks run in parallel:
17+
# bzlmod-tidy-check — runs `bazel mod tidy` and fails if it would change any file
18+
# bzlmod-lockfile-check — runs `bazel mod deps --lockfile_mode=error` and fails if the
19+
# lockfile does not match the resolved dependency graph
20+
#
21+
# Recommended alternative: run these checks locally via pre-commit so issues are caught
22+
# before they reach CI. Add the following to your .pre-commit-config.yaml:
23+
#
24+
# - repo: local
25+
# hooks:
26+
# - id: bzlmod-tidy
27+
# name: bazel mod tidy
28+
# entry: bazel mod tidy
29+
# language: system
30+
# pass_filenames: false
31+
# - id: bzlmod-lockfile
32+
# name: bazel mod deps lockfile check
33+
# entry: bazel mod deps --lockfile_mode=error
34+
# language: system
35+
# pass_filenames: false
36+
#
37+
# Security note: this workflow refuses to run on pull_request_target. That event has
38+
# write access to repository secrets while checking out fork code, making it unsafe
39+
# to execute Bazel on untrusted input. Use pull_request or schedule instead.
40+
1441
name: Bazel Bzlmod Lockfile Check
1542

1643
on:
@@ -23,14 +50,49 @@ on:
2350
type: string
2451

2552
jobs:
26-
bzlmod-lock-check:
53+
bzlmod-tidy-check:
2754
runs-on: ${{ vars.runner_labels_ghub_standard_x64 && fromJSON(vars.runner_labels_ghub_standard_x64) || vars.REPO_RUNNER_LABELS && fromJSON(vars.REPO_RUNNER_LABELS) || 'ubuntu-latest' }}
2855
steps:
29-
- name: Checkout repository (Handle all events)
56+
- name: Refuse to run on pull_request_target
57+
if: github.event_name == 'pull_request_target'
58+
run: |
59+
echo "This workflow must not be called from pull_request_target."
60+
echo "That event has write access to repo secrets while checking out fork code,"
61+
echo "making it unsafe to run bazel on untrusted input."
62+
echo "Use pull_request or schedule instead."
63+
exit 1
64+
65+
- name: Checkout repository
3066
uses: actions/checkout@v4.2.2
67+
68+
- name: Setup Bazel with shared caching
69+
uses: bazel-contrib/setup-bazel@0.18.0
3170
with:
32-
ref: ${{ github.head_ref || github.event.pull_request.head.ref || github.ref }}
33-
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
71+
disk-cache: false
72+
repository-cache: false
73+
bazelisk-cache: true
74+
cache-save: ${{ github.event_name == 'push' }}
75+
76+
- name: Check MODULE.bazel formatting
77+
working-directory: ${{ inputs.working-directory }}
78+
run: |
79+
bazel mod tidy
80+
git diff --exit-code
81+
82+
bzlmod-lockfile-check:
83+
runs-on: ${{ vars.runner_labels_ghub_standard_x64 && fromJSON(vars.runner_labels_ghub_standard_x64) || vars.REPO_RUNNER_LABELS && fromJSON(vars.REPO_RUNNER_LABELS) || 'ubuntu-latest' }}
84+
steps:
85+
- name: Refuse to run on pull_request_target
86+
if: github.event_name == 'pull_request_target'
87+
run: |
88+
echo "This workflow must not be called from pull_request_target."
89+
echo "That event has write access to repo secrets while checking out fork code,"
90+
echo "making it unsafe to run bazel on untrusted input."
91+
echo "Use pull_request or schedule instead."
92+
exit 1
93+
94+
- name: Checkout repository
95+
uses: actions/checkout@v4.2.2
3496

3597
- name: Setup Bazel with shared caching
3698
uses: bazel-contrib/setup-bazel@0.18.0
@@ -40,7 +102,7 @@ jobs:
40102
bazelisk-cache: true
41103
cache-save: ${{ github.event_name == 'push' }}
42104

43-
- name: Verify MODULE.bazel.lock exists
105+
- name: Verify MODULE.bazel and MODULE.bazel.lock exist
44106
working-directory: ${{ inputs.working-directory }}
45107
run: |
46108
if [ ! -f "MODULE.bazel" ]; then
@@ -53,10 +115,6 @@ jobs:
53115
exit 1
54116
fi
55117
56-
- name: Run bazel mod tidy
57-
working-directory: ${{ inputs.working-directory }}
58-
run: bazel mod tidy
59-
60-
- name: Check lock consistency
118+
- name: Check lockfile is up to date
61119
working-directory: ${{ inputs.working-directory }}
62-
run: git diff --exit-code -- MODULE.bazel MODULE.bazel.lock
120+
run: bazel mod deps --lockfile_mode=error

README.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,29 @@ jobs:
511511

512512
### **16. Bzlmod Lockfile Check Workflow**
513513

514-
This workflow keeps Bazel's lockfile in sync with your module declarations. It records the exact set of resolved module versions and extension outputs so builds are reproducible across machines. The job fails if the lockfile is missing or out of date after running `bazel mod tidy`, which means someone changed `MODULE.bazel` without updating the lockfile.
514+
This workflow keeps `MODULE.bazel` and `MODULE.bazel.lock` consistent and reproducible. Two checks run in parallel:
515+
516+
- **`bzlmod-tidy-check`** — runs `bazel mod tidy` and fails if it would change any file, meaning `MODULE.bazel` has formatting or dependency declarations that are not normalized
517+
- **`bzlmod-lockfile-check`** — runs `bazel mod deps --lockfile_mode=error` and fails if the committed lockfile does not match the resolved dependency graph, meaning the lockfile is stale
518+
519+
> ⚠️ **Security:** this workflow refuses to run when called from `pull_request_target`. That event has write access to repository secrets while checking out fork code, making it unsafe to execute Bazel on untrusted input. Use `pull_request` or `schedule` instead.
520+
521+
> 💡 **Recommendation:** run these checks locally via [pre-commit](https://pre-commit.com/) so issues are caught before they reach CI:
522+
>
523+
> ```yaml
524+
> - repo: local
525+
> hooks:
526+
> - id: bzlmod-tidy
527+
> name: bazel mod tidy
528+
> entry: bazel mod tidy
529+
> language: system
530+
> pass_filenames: false
531+
> - id: bzlmod-lockfile
532+
> name: bazel mod deps lockfile check
533+
> entry: bazel mod deps --lockfile_mode=error
534+
> language: system
535+
> pass_filenames: false
536+
> ```
515537

516538
**Usage Example**
517539

@@ -528,16 +550,17 @@ jobs:
528550
bzlmod-lock:
529551
uses: eclipse-score/cicd-workflows/.github/workflows/bzlmod-lock-check.yml@main
530552
with:
531-
working-directory: .
553+
working-directory: . # optional, this is the default
532554
```
533555

534556
**Defaults**
535-
- `working-directory`: `.`
557+
- `working-directory`: `.`
536558

537559
This workflow:
538560
✅ Fails if `MODULE.bazel.lock` is missing
539-
✅ Runs `bazel mod tidy`
540-
✅ Fails if `MODULE.bazel` or `MODULE.bazel.lock` changes after tidy
561+
✅ Fails if `bazel mod tidy` would change `MODULE.bazel` or `MODULE.bazel.lock`
562+
✅ Fails if `bazel mod deps --lockfile_mode=error` reports a stale or inconsistent lockfile
563+
✅ Reports both failures independently in the PR checks UI
541564

542565
---
543566

0 commit comments

Comments
 (0)