Skip to content

Commit 9070a69

Browse files
chore: add llm pr janitor n cicd
1 parent 099a5f3 commit 9070a69

File tree

9 files changed

+604
-0
lines changed

9 files changed

+604
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
name: Update PR Description with Rigging
3+
4+
on:
5+
pull_request:
6+
types: [opened, synchronize]
7+
8+
jobs:
9+
update-description:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
pull-requests: write
13+
contents: read
14+
15+
steps:
16+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
17+
with:
18+
fetch-depth: 0 # full history for proper diffing
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
22+
with:
23+
python-version: "3.10"
24+
25+
- name: Install uv
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install uv
29+
30+
- name: Generate PR Description
31+
id: description
32+
env:
33+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
34+
run: |
35+
DESCRIPTION="$(uv run --no-project .hooks/generate_pr_description.py --base-ref "origin/${{ github.base_ref }}" --exclude "./*.lock")"
36+
{
37+
echo "description<<EOF"
38+
echo "${DESCRIPTION}"
39+
echo "EOF"
40+
} >> "$GITHUB_OUTPUT"
41+
42+
- name: Update PR Description
43+
uses: nefrob/pr-description@4dcc9f3ad5ec06b2a197c5f8f93db5e69d2fdca7 # v1.2.0
44+
with:
45+
token: ${{ secrets.GITHUB_TOKEN }}
46+
content: |
47+
48+
---
49+
50+
## Generated Summary
51+
52+
${{ steps.description.outputs.description }}
53+
54+
This summary was generated with ❤️ by [rigging](https://docs.dreadnode.io/rigging/)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
name: "Semantic Lints PR"
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
types:
8+
- opened
9+
- edited
10+
- synchronize
11+
- reopened
12+
13+
permissions:
14+
pull-requests: read
15+
16+
jobs:
17+
main:
18+
name: Validate PR title
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
22+
env:
23+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.hooks/.gitkeep

Whitespace-only changes.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python
2+
import re
3+
import sys
4+
from pathlib import Path
5+
6+
7+
class GitHubActionChecker:
8+
def __init__(self) -> None:
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 format_terminal_link(self, file_path: str, line_number: int) -> str:
21+
"""Format a terminal link to a file and line number.
22+
23+
Args:
24+
file_path: Path to the file
25+
line_number: Line number in the file
26+
27+
Returns:
28+
str: Formatted string with file path and line number
29+
"""
30+
return f"{file_path}:{line_number}"
31+
32+
def get_line_numbers(self, content: str, pattern: re.Pattern[str]) -> list[tuple[str, int]]:
33+
"""Find matches with their line numbers."""
34+
matches = []
35+
matches.extend(
36+
(match.group(0), i)
37+
for i, line in enumerate(content.splitlines(), 1)
38+
for match in pattern.finditer(line)
39+
)
40+
return matches
41+
42+
def check_file(self, file_path: str) -> bool:
43+
"""Check a single file for unpinned dependencies."""
44+
try:
45+
content = Path(file_path).read_text()
46+
except (FileNotFoundError, PermissionError, IsADirectoryError, OSError) as e:
47+
print(f"\033[91mError reading file {file_path}: {e}\033[0m")
48+
return False
49+
50+
# Get matches with line numbers
51+
pinned_matches = self.get_line_numbers(content, self.pinned_pattern)
52+
unpinned_matches = self.get_line_numbers(content, self.unpinned_pattern)
53+
all_matches = self.get_line_numbers(content, self.all_uses_pattern)
54+
55+
print(f"\n\033[1m[=] Checking file: {file_path}\033[0m")
56+
57+
# Print pinned dependencies
58+
if pinned_matches:
59+
print("\033[92m[+] Pinned:\033[0m")
60+
for match, line_num in pinned_matches:
61+
print(f" |- {match} \033[90m({file_path}:{line_num})\033[0m")
62+
63+
# Track all found actions for validation
64+
found_actions = set()
65+
for match, _ in pinned_matches + unpinned_matches:
66+
action_name = self.pinned_pattern.match(match) or self.unpinned_pattern.match(match)
67+
if action_name:
68+
found_actions.add(action_name.group(1))
69+
70+
has_errors = False
71+
72+
# Check for unpinned dependencies
73+
if unpinned_matches:
74+
has_errors = True
75+
print("\033[93m[!] Unpinned (using version tags):\033[0m")
76+
for match, line_num in unpinned_matches:
77+
print(f" |- {match} \033[90m({file_path}:{line_num})\033[0m")
78+
79+
# Check for completely unpinned dependencies (no SHA or version)
80+
unpinned_without_hash = [
81+
(match, line_num)
82+
for match, line_num in all_matches
83+
if not any(match in pinned[0] for pinned in pinned_matches)
84+
and not any(match in unpinned[0] for unpinned in unpinned_matches)
85+
]
86+
87+
if unpinned_without_hash:
88+
has_errors = True
89+
print("\033[91m[!] Completely unpinned (no SHA or version):\033[0m")
90+
for match, line_num in unpinned_without_hash:
91+
print(
92+
f" |- {match} \033[90m({self.format_terminal_link(file_path, line_num)})\033[0m",
93+
)
94+
95+
# Print summary
96+
total_actions = len(pinned_matches) + len(unpinned_matches) + len(unpinned_without_hash)
97+
if total_actions == 0:
98+
print("\033[93m[!] No GitHub Actions found in this file\033[0m")
99+
else:
100+
print("\n\033[1mSummary:\033[0m")
101+
print(f"Total actions: {total_actions}")
102+
print(f"Pinned: {len(pinned_matches)}")
103+
print(f"Unpinned with version: {len(unpinned_matches)}")
104+
print(f"Completely unpinned: {len(unpinned_without_hash)}")
105+
106+
return not has_errors
107+
108+
109+
def main() -> None:
110+
checker = GitHubActionChecker()
111+
files_to_check = sys.argv[1:]
112+
113+
if not files_to_check:
114+
print("\033[91mError: No files provided to check\033[0m")
115+
print("Usage: python script.py <file1> <file2> ...")
116+
sys.exit(1)
117+
118+
results = {file: checker.check_file(file) for file in files_to_check}
119+
120+
# Print final summary
121+
print("\n\033[1mFinal Results:\033[0m")
122+
for file, passed in results.items():
123+
status = "\033[92m✓ Passed\033[0m" if passed else "\033[91m✗ Failed\033[0m"
124+
print(f"{status} {file}")
125+
126+
if not all(results.values()):
127+
sys.exit(1)
128+
129+
130+
if __name__ == "__main__":
131+
main()

0 commit comments

Comments
 (0)