Skip to content

Commit 8f5effe

Browse files
committed
Implement checkout reference validation and enhance README documentation
- Added a Python script to check for 'with.ref' specification in all actions/checkout usages, ensuring consistent checkout behavior across workflows. - Introduced a new lint-workflows.yml file to automate the validation process during push and pull request events. - Updated the README to include new security requirements and examples for specifying 'with.ref' in workflows, reinforcing best practices for GitHub Actions.
1 parent 4a2bfd8 commit 8f5effe

File tree

3 files changed

+134
-3
lines changed

3 files changed

+134
-3
lines changed

.github/README.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ GitHub's `pull_request` event doesn't expose secrets to fork PRs (for security).
1616
### Security Requirements
1717

1818
1. **Always use `approval-gate.yml`** as a dependency for jobs needing secrets
19-
2. **Always use `target-checkout`** action to checkout the correct PR commit (not the base branch)
20-
3. **Always pass the approval gate's `commit-sha`** to prevent testing unapproved code:
19+
2. **Always specify `with.ref`** on all `actions/checkout` steps (enforced by `lint-workflows.yml`)
20+
3. **Always pass the approval gate's `commit-sha`** to prevent testing unapproved code
21+
22+
### Checkout Patterns
23+
24+
**For workflows using approval-gate** (recommended for `pull_request_target`):
2125

2226
```yaml
2327
jobs:
@@ -27,9 +31,23 @@ jobs:
2731
build:
2832
needs: approval-gate
2933
steps:
30-
- uses: ./.github/actions/target-checkout
34+
- uses: actions/checkout@v6
3135
with:
3236
ref: ${{ needs.approval-gate.outputs.commit-sha }}
3337
```
3438
39+
**For simpler workflows** (e.g., `pull_request` or `push` triggers):
40+
41+
```yaml
42+
# Preferred: Define ref once at workflow level, reuse in all jobs
43+
env:
44+
CHECKOUT_REF: ${{ github.ref }}
45+
jobs:
46+
build:
47+
steps:
48+
- uses: actions/checkout@v6
49+
with:
50+
ref: ${{ env.CHECKOUT_REF }}
51+
```
52+
3553
> ⚠️ Without these safeguards, a malicious commit could be added after approval but before execution.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Check that all actions/checkout usages have 'with.ref' specified.
4+
5+
This ensures consistent and explicit checkout behavior across all workflows.
6+
"""
7+
8+
import sys
9+
from pathlib import Path
10+
11+
import yaml
12+
13+
14+
def check_workflow_file(filepath: Path) -> list[dict]:
15+
"""
16+
Check a workflow file for actions/checkout usages without 'with.ref'.
17+
18+
Returns a list of violations.
19+
"""
20+
violations = []
21+
22+
with open(filepath, "r") as f:
23+
try:
24+
data = yaml.safe_load(f)
25+
except yaml.YAMLError as e:
26+
print(f"Warning: Failed to parse {filepath}: {e}")
27+
return []
28+
29+
if not data or "jobs" not in data:
30+
return []
31+
32+
for job_name, job in data["jobs"].items():
33+
steps = job.get("steps", [])
34+
for i, step in enumerate(steps):
35+
uses = step.get("uses", "")
36+
if "actions/checkout" in uses:
37+
with_block = step.get("with", {})
38+
has_ref = isinstance(with_block, dict) and "ref" in with_block
39+
40+
if not has_ref:
41+
violations.append({
42+
"file": str(filepath),
43+
"job": job_name,
44+
"step": i,
45+
"uses": uses,
46+
})
47+
48+
return violations
49+
50+
51+
def main():
52+
workflows_dir = Path(".github/workflows")
53+
54+
if not workflows_dir.exists():
55+
print("Error: .github/workflows directory not found")
56+
sys.exit(1)
57+
58+
all_violations = []
59+
60+
for pattern in ("*.yml", "*.yaml"):
61+
for workflow_file in sorted(workflows_dir.glob(pattern)):
62+
all_violations.extend(check_workflow_file(workflow_file))
63+
64+
if all_violations:
65+
print("❌ Found actions/checkout usages without 'with.ref' specified:\n")
66+
for v in all_violations:
67+
print(f" {v['file']}")
68+
print(f" Job: {v['job']}, Step: {v['step']}")
69+
print(f" Uses: {v['uses']}\n")
70+
print(f"Total violations: {len(all_violations)}")
71+
print("\nAll actions/checkout steps should specify 'with.ref' to ensure")
72+
print("consistent and explicit checkout behavior.")
73+
print("\nSee .github/README.md for security requirements and examples.")
74+
sys.exit(1)
75+
else:
76+
print("✅ All actions/checkout usages have 'with.ref' specified")
77+
sys.exit(0)
78+
79+
80+
if __name__ == "__main__":
81+
main()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Lint Workflows
2+
permissions:
3+
contents: read
4+
on:
5+
push:
6+
paths:
7+
- '.github/workflows/**'
8+
- '.github/scripts/**'
9+
pull_request:
10+
paths:
11+
- '.github/workflows/**'
12+
- '.github/scripts/**'
13+
14+
jobs:
15+
check-checkout-ref:
16+
name: Check actions/checkout has ref specified
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v6
20+
with:
21+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: '3.14'
27+
28+
- name: Install dependencies
29+
run: pip install pyyaml
30+
31+
- name: Check all actions/checkout have ref specified
32+
run: python .github/scripts/check-checkout-ref.py

0 commit comments

Comments
 (0)