Skip to content

Commit c8458a5

Browse files
chore: add some basic precommits (#522)
1 parent baff712 commit c8458a5

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import re
2+
import sys
3+
from pathlib import Path
4+
from typing import List, Tuple
5+
6+
7+
class GitHubActionChecker:
8+
def __init__(self):
9+
# Pattern for actions with SHA-1 hashes (pinned)
10+
self.pinned_pattern = re.compile(r"uses:\s+([^@\s]+)@([a-f0-9]{40})")
11+
12+
# Pattern for actions with version tags (unpinned)
13+
self.unpinned_pattern = re.compile(
14+
r"uses:\s+([^@\s]+)@(v\d+(?:\.\d+)*(?:-[a-zA-Z0-9]+(?:\.\d+)*)?)"
15+
)
16+
17+
# Pattern for all uses statements
18+
self.all_uses_pattern = re.compile(r"uses:\s+([^@\s]+)@([^\s\n]+)")
19+
20+
def get_line_numbers(
21+
self, content: str, pattern: re.Pattern
22+
) -> List[Tuple[str, int]]:
23+
"""Find matches with their line numbers."""
24+
matches = []
25+
for i, line in enumerate(content.splitlines(), 1):
26+
for match in pattern.finditer(line):
27+
matches.append((match.group(0), i))
28+
return matches
29+
30+
def check_file(self, file_path: str) -> bool:
31+
"""Check a single file for unpinned dependencies."""
32+
try:
33+
content = Path(file_path).read_text()
34+
except Exception as e:
35+
print(f"\033[91mError reading file {file_path}: {e}\033[0m")
36+
return False
37+
38+
# Get matches with line numbers
39+
pinned_matches = self.get_line_numbers(content, self.pinned_pattern)
40+
unpinned_matches = self.get_line_numbers(content, self.unpinned_pattern)
41+
all_matches = self.get_line_numbers(content, self.all_uses_pattern)
42+
43+
print(f"\n\033[1m[=] Checking file: {file_path}\033[0m")
44+
45+
# Print pinned dependencies
46+
if pinned_matches:
47+
print("\033[92m[+] Pinned:\033[0m")
48+
for match, line_num in pinned_matches:
49+
print(f" |- {match} \033[90m({file_path}:{line_num})\033[0m")
50+
51+
# Track all found actions for validation
52+
found_actions = set()
53+
for match, _ in pinned_matches + unpinned_matches:
54+
action_name = self.pinned_pattern.match(
55+
match
56+
) or self.unpinned_pattern.match(match)
57+
if action_name:
58+
found_actions.add(action_name.group(1))
59+
60+
has_errors = False
61+
62+
# Check for unpinned dependencies
63+
if unpinned_matches:
64+
has_errors = True
65+
print("\033[93m[!] Unpinned (using version tags):\033[0m")
66+
for match, line_num in unpinned_matches:
67+
print(f" |- {match} \033[90m({file_path}:{line_num})\033[0m")
68+
69+
# Check for completely unpinned dependencies (no SHA or version)
70+
unpinned_without_hash = [
71+
(match, line_num)
72+
for match, line_num in all_matches
73+
if not any(match in pinned[0] for pinned in pinned_matches)
74+
and not any(match in unpinned[0] for unpinned in unpinned_matches)
75+
]
76+
77+
if unpinned_without_hash:
78+
has_errors = True
79+
print("\033[91m[!] Completely unpinned (no SHA or version):\033[0m")
80+
for match, line_num in unpinned_without_hash:
81+
print(
82+
f" |- {match} \033[90m({self.format_terminal_link(file_path, line_num)})\033[0m"
83+
)
84+
85+
# Print summary
86+
total_actions = (
87+
len(pinned_matches) + len(unpinned_matches) + len(unpinned_without_hash)
88+
)
89+
if total_actions == 0:
90+
print("\033[93m[!] No GitHub Actions found in this file\033[0m")
91+
else:
92+
print("\n\033[1mSummary:\033[0m")
93+
print(f"Total actions: {total_actions}")
94+
print(f"Pinned: {len(pinned_matches)}")
95+
print(f"Unpinned with version: {len(unpinned_matches)}")
96+
print(f"Completely unpinned: {len(unpinned_without_hash)}")
97+
98+
return not has_errors
99+
100+
101+
def main():
102+
checker = GitHubActionChecker()
103+
files_to_check = sys.argv[1:]
104+
105+
if not files_to_check:
106+
print("\033[91mError: No files provided to check\033[0m")
107+
print("Usage: python script.py <file1> <file2> ...")
108+
sys.exit(1)
109+
110+
results = {file: checker.check_file(file) for file in files_to_check}
111+
112+
# Print final summary
113+
print("\n\033[1mFinal Results:\033[0m")
114+
for file, passed in results.items():
115+
status = "\033[92m✓ Passed\033[0m" if passed else "\033[91m✗ Failed\033[0m"
116+
print(f"{status} {file}")
117+
118+
if not all(results.values()):
119+
sys.exit(1)
120+
121+
122+
if __name__ == "__main__":
123+
main()

pre-commit-config.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
repos:
2+
# Standard pre-commit hooks
3+
- repo: https://github.com/pre-commit/pre-commit-hooks
4+
rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b #v5.0.0
5+
hooks:
6+
- id: check-added-large-files
7+
args: [--maxkb=36000]
8+
- id: check-json
9+
- id: check-yaml
10+
- id: trailing-whitespace
11+
12+
# Github actions
13+
- repo: https://github.com/rhysd/actionlint
14+
rev: 5db9d9cde2f3deb5035dea3e45f0a9fff2f29448
15+
hooks:
16+
- id: actionlint
17+
name: Check Github Actions
18+
19+
# Secrets detection
20+
- repo: https://github.com/Yelp/detect-secrets
21+
rev: 01886c8a910c64595c47f186ca1ffc0b77fa5458
22+
hooks:
23+
- id: detect-secrets
24+
25+
- repo: local
26+
hooks:
27+
# Ensure GH actions are pinned to a specific hash
28+
- id: check-github-actions
29+
name: Check GitHub Actions for Pinned Dependencies
30+
entry: python .scripts/check_pinned_hash_dependencies.py
31+
language: python
32+
files: \.github/.*\.yml$

0 commit comments

Comments
 (0)