Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/rigging_pr_description.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
name: Update PR Description with Rigging

on:
pull_request:
types: [opened, synchronize]

jobs:
update-description:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0 # full history for proper diffing

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.10"

- name: Install uv
run: |
python -m pip install --upgrade pip
pip install uv

- name: Generate PR Description
id: description
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
DESCRIPTION="$(uv run --no-project .hooks/generate_pr_description.py --base-ref "origin/${{ github.base_ref }}" --exclude "./*.lock")"
{
echo "description<<EOF"
echo "${DESCRIPTION}"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Update PR Description
uses: nefrob/pr-description@4dcc9f3ad5ec06b2a197c5f8f93db5e69d2fdca7 # v1.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
content: |

---

## Generated Summary

${{ steps.description.outputs.description }}

This summary was generated with ❤️ by [rigging](https://docs.dreadnode.io/rigging/)
23 changes: 23 additions & 0 deletions .github/workflows/semantic-prs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
name: "Semantic Lints PR"
on:
pull_request:
branches:
- main
types:
- opened
- edited
- synchronize
- reopened

permissions:
pull-requests: read

jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Empty file added .hooks/.gitkeep
Empty file.
131 changes: 131 additions & 0 deletions .hooks/check_pinned_hash_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env python
import re
import sys
from pathlib import Path


class GitHubActionChecker:
def __init__(self) -> None:
# Pattern for actions with SHA-1 hashes (pinned)
self.pinned_pattern = re.compile(r"uses:\s+([^@\s]+)@([a-f0-9]{40})")

# Pattern for actions with version tags (unpinned)
self.unpinned_pattern = re.compile(
r"uses:\s+([^@\s]+)@(v\d+(?:\.\d+)*(?:-[a-zA-Z0-9]+(?:\.\d+)*)?)",
)

# Pattern for all uses statements
self.all_uses_pattern = re.compile(r"uses:\s+([^@\s]+)@([^\s\n]+)")

def format_terminal_link(self, file_path: str, line_number: int) -> str:
"""Format a terminal link to a file and line number.

Args:
file_path: Path to the file
line_number: Line number in the file

Returns:
str: Formatted string with file path and line number
"""
return f"{file_path}:{line_number}"

def get_line_numbers(self, content: str, pattern: re.Pattern[str]) -> list[tuple[str, int]]:
"""Find matches with their line numbers."""
matches = []
matches.extend(
(match.group(0), i)
for i, line in enumerate(content.splitlines(), 1)
for match in pattern.finditer(line)
)
return matches

def check_file(self, file_path: str) -> bool:
"""Check a single file for unpinned dependencies."""
try:
content = Path(file_path).read_text()
except (FileNotFoundError, PermissionError, IsADirectoryError, OSError) as e:
print(f"\033[91mError reading file {file_path}: {e}\033[0m")
return False

# Get matches with line numbers
pinned_matches = self.get_line_numbers(content, self.pinned_pattern)
unpinned_matches = self.get_line_numbers(content, self.unpinned_pattern)
all_matches = self.get_line_numbers(content, self.all_uses_pattern)

print(f"\n\033[1m[=] Checking file: {file_path}\033[0m")

# Print pinned dependencies
if pinned_matches:
print("\033[92m[+] Pinned:\033[0m")
for match, line_num in pinned_matches:
print(f" |- {match} \033[90m({file_path}:{line_num})\033[0m")

# Track all found actions for validation
found_actions = set()
for match, _ in pinned_matches + unpinned_matches:
action_name = self.pinned_pattern.match(match) or self.unpinned_pattern.match(match)
if action_name:
found_actions.add(action_name.group(1))

has_errors = False

# Check for unpinned dependencies
if unpinned_matches:
has_errors = True
print("\033[93m[!] Unpinned (using version tags):\033[0m")
for match, line_num in unpinned_matches:
print(f" |- {match} \033[90m({file_path}:{line_num})\033[0m")

# Check for completely unpinned dependencies (no SHA or version)
unpinned_without_hash = [
(match, line_num)
for match, line_num in all_matches
if not any(match in pinned[0] for pinned in pinned_matches)
and not any(match in unpinned[0] for unpinned in unpinned_matches)
]

if unpinned_without_hash:
has_errors = True
print("\033[91m[!] Completely unpinned (no SHA or version):\033[0m")
for match, line_num in unpinned_without_hash:
print(
f" |- {match} \033[90m({self.format_terminal_link(file_path, line_num)})\033[0m",
)

# Print summary
total_actions = len(pinned_matches) + len(unpinned_matches) + len(unpinned_without_hash)
if total_actions == 0:
print("\033[93m[!] No GitHub Actions found in this file\033[0m")
else:
print("\n\033[1mSummary:\033[0m")
print(f"Total actions: {total_actions}")
print(f"Pinned: {len(pinned_matches)}")
print(f"Unpinned with version: {len(unpinned_matches)}")
print(f"Completely unpinned: {len(unpinned_without_hash)}")

return not has_errors


def main() -> None:
checker = GitHubActionChecker()
files_to_check = sys.argv[1:]

if not files_to_check:
print("\033[91mError: No files provided to check\033[0m")
print("Usage: python script.py <file1> <file2> ...")
sys.exit(1)

results = {file: checker.check_file(file) for file in files_to_check}

# Print final summary
print("\n\033[1mFinal Results:\033[0m")
for file, passed in results.items():
status = "\033[92m✓ Passed\033[0m" if passed else "\033[91m✗ Failed\033[0m"
print(f"{status} {file}")

if not all(results.values()):
sys.exit(1)


if __name__ == "__main__":
main()
Loading
Loading