Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a630d4e
feat: configure git to use GITHUB_TOKEN for authentication in update …
ColinDaglish Sep 10, 2026
7436743
feat: update git push command to use --force-with-lease for safer bra…
ColinDaglish Sep 10, 2026
08da0d1
feat: update setup-python action to version 7.0.0 for improved functi…
ColinDaglish Sep 10, 2026
c2c03f5
feat: update git configuration to use GITHUB_TOKEN via HTTP header fo…
ColinDaglish Sep 10, 2026
60e4e3c
feat: update git push command to use token in URL for improved security
ColinDaglish Sep 10, 2026
eb4e22b
feat: update push command to use GitHub CLI for authentication and si…
ColinDaglish Sep 10, 2026
05385b3
feat: refine update detection to only track semantic versioned releas…
ColinDaglish Sep 10, 2026
a7a66e8
feat: add force update option to bypass cooldown periods in PR genera…
ColinDaglish Sep 10, 2026
8a6116b
feat: replace pyyaml with ruamel.yaml for improved YAML handling and …
ColinDaglish Sep 10, 2026
5b7ac71
fix: revert to pyyaml for dependency installation and YAML parsing
ColinDaglish Sep 10, 2026
5d48b91
fix: handle potential errors when adding comments to repo entries
ColinDaglish Sep 10, 2026
74ef6e0
refactor: remove inline comment addition for version tracking in repo…
ColinDaglish Sep 10, 2026
63290a6
fix: skip updates if version hasn't changed to prevent unnecessary pr…
ColinDaglish Sep 10, 2026
0c7c66c
refactor: remove workflow_dispatch inputs for cooldown periods and sk…
ColinDaglish Sep 10, 2026
29f4dd3
fix: update inline comment for frozen version in repo entries and han…
ColinDaglish Sep 10, 2026
cad9b6d
feat: add workflow_dispatch inputs for cooldown periods and hook skip…
ColinDaglish Sep 10, 2026
b8b036e
chore(pre-commit): auto-update hooks
github-actions[bot] Sep 10, 2026
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
143 changes: 77 additions & 66 deletions .github/workflows/auto-update-precommit-hooks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"

Expand Down Expand Up @@ -142,29 +142,6 @@ jobs:

return None

def get_latest_commit_sha(repo_url: str, branch: str = 'HEAD') -> Optional[str]:
"""Fetch latest commit SHA from a repository."""
try:
match = re.search(r'github\.com/([^/]+)/(.+?)(?:\.git)?$', repo_url)
if not match:
return None

owner, repo = match.groups()

cmd = [
'gh', 'api', '--paginate',
f'repos/{owner}/{repo}/commits',
'-q', '.[0].sha'
]

result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()
except Exception as e:
print(f"Warning: Error fetching latest commit for {repo_url}: {e}")

return None

def resolve_sha_to_tag(repo_url: str, sha: str) -> Optional[str]:
"""Resolve a commit SHA to its tag, if one exists."""
try:
Expand Down Expand Up @@ -236,7 +213,8 @@ jobs:

print(f"Checking updates for: {repo_url}")

# Try to get latest release first
# Only check tagged releases (Dependabot behavior)
# Skips repositories without semantic versioning
release_info = get_latest_release(repo_url)

if release_info:
Expand All @@ -257,6 +235,11 @@ jobs:
if not old_version:
old_version = current_sha[:7]

# Skip if version hasn't actually changed (SHA may differ but tag is same)
if old_version == latest_tag:
print(f" ⊘ Skipped: Version unchanged ({old_version})")
continue

updates.append({
'repo': repo_url,
'old_sha': current_sha,
Expand All @@ -268,19 +251,9 @@ jobs:
})
print(f" Update available: {old_version} -> {latest_tag}")
else:
# Fall back to latest commit if no releases
latest_sha = get_latest_commit_sha(repo_url)
if latest_sha and latest_sha != current_sha:
print(f" Update available (commit): {current_sha[:7]} -> {latest_sha[:7]}")
updates.append({
'repo': repo_url,
'old_sha': current_sha,
'new_sha': latest_sha,
'old_version': current_sha[:7],
'new_version': latest_sha[:7],
'semver_level': 'patch', # Default to patch for commits
'commit_range': f'{current_sha}...{latest_sha}'
})
# No tagged releases found - skip this repo (Dependabot-aligned behavior)
# This ensures we only track semantic versioned hooks
print(f" ⊘ Skipped: No tagged releases found (only tagged versions are tracked, like Dependabot)")

# Output results
if updates:
Expand Down Expand Up @@ -312,13 +285,13 @@ jobs:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"

- name: Install dependencies
run: |
pip install pyyaml
pip install ruamel.yaml

- name: Apply cooldown filters
id: cooldown
Expand Down Expand Up @@ -421,7 +394,7 @@ jobs:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"

Expand Down Expand Up @@ -539,34 +512,36 @@ jobs:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"

- name: Install dependencies
run: |
pip install pyyaml
pip install ruamel.yaml

- name: Update configs and create PR
env:
RELEASE_INFO: ${{ needs.fetch-release-info.outputs.release_info }}
SKIPPED_UPDATES: ${{ needs.apply-cooldown.outputs.skipped_updates }}
GITHUB_TOKEN: ${{ github.token }}
FORCE_UPDATE: ${{ github.event.inputs.force_update || false }}
COOLDOWN_MAJOR: ${{ github.event.inputs.cooldown_major_days || '28' }}
COOLDOWN_MINOR: ${{ github.event.inputs.cooldown_minor_days || '14' }}
COOLDOWN_PATCH: ${{ github.event.inputs.cooldown_patch_days || '7' }}
run: |
python3 << 'EOF'
import json
import os
import yaml
import subprocess
import re
from datetime import datetime, timezone
from ruamel.yaml import YAML

# Load release info
release_info = json.loads(os.environ['RELEASE_INFO'])
skipped = json.loads(os.environ['SKIPPED_UPDATES'] or '[]')
force_update = os.environ.get('FORCE_UPDATE', 'false').lower() == 'true'

cooldown_config = {
'major': int(os.environ['COOLDOWN_MAJOR']),
Expand All @@ -579,16 +554,20 @@ jobs:
print("No updates to apply")
exit(0)

# Load and update .pre-commit-config.yaml
# Load and update .pre-commit-config.yaml with comment preservation
yaml = YAML()
yaml.preserve_quotes = True
yaml.default_flow_style = False

with open('.pre-commit-config.yaml', 'r') as f:
config = yaml.safe_load(f)
config = yaml.load(f)

# Update tracking file
try:
with open('configs/precommit-update-tracking.json', 'r') as f:
tracking = json.load(f)
except FileNotFoundError:
tracking = {'last_updated': datetime.utcnow().isoformat() + 'Z', 'hooks': {}}
tracking = {'last_updated': datetime.now(timezone.utc).isoformat() + 'Z', 'hooks': {}}

# Create a mapping of repo URLs to new SHAs
update_map = {update['repo']: update for update in release_info}
Expand All @@ -600,29 +579,45 @@ jobs:
update = update_map[repo_url]
repo_entry['rev'] = update['new_sha']

# Update inline comment with frozen version
try:
from ruamel.yaml.comments import CommentedMap
if hasattr(repo_entry, 'ca'):
# Update the comment on the 'rev' key with the new version
repo_entry.ca.items['rev'] = [None, None, None, f' frozen: {update["new_version"]}']
except Exception as e:
# Comment update failed - remove the stale comment entirely
# Better to have no comment than an incorrect frozen version tag
print(f"Warning: Could not update comment for {repo_url}, removing stale comment: {e}")
try:
if hasattr(repo_entry, 'ca') and 'rev' in repo_entry.ca.items:
repo_entry.ca.items['rev'] = [None, None, None, None]
except:
pass # If we can't even remove it, continue without comment

# Update tracking
if repo_url not in tracking['hooks']:
tracking['hooks'][repo_url] = {
'current_sha': update['new_sha'],
'current_version': update['new_version'],
'semver_levels': {
'major': datetime.utcnow().isoformat() + 'Z',
'minor': datetime.utcnow().isoformat() + 'Z',
'patch': datetime.utcnow().isoformat() + 'Z'
'major': datetime.now(timezone.utc).isoformat() + 'Z',
'minor': datetime.now(timezone.utc).isoformat() + 'Z',
'patch': datetime.now(timezone.utc).isoformat() + 'Z'
}
}
else:
semver = update['semver_level']
tracking['hooks'][repo_url]['current_sha'] = update['new_sha']
tracking['hooks'][repo_url]['current_version'] = update['new_version']
if semver != 'unknown':
tracking['hooks'][repo_url]['semver_levels'][semver] = datetime.utcnow().isoformat() + 'Z'
tracking['hooks'][repo_url]['semver_levels'][semver] = datetime.now(timezone.utc).isoformat() + 'Z'

tracking['hooks'][repo_url]['last_updated'] = datetime.utcnow().isoformat() + 'Z'
tracking['hooks'][repo_url]['last_updated'] = datetime.now(timezone.utc).isoformat() + 'Z'

# Write updated files
# Write updated files with comment preservation
with open('.pre-commit-config.yaml', 'w') as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
yaml.dump(config, f)

with open('configs/precommit-update-tracking.json', 'w') as f:
json.dump(tracking, f, indent=2)
Expand All @@ -635,7 +630,7 @@ jobs:
return match.group(1)
return repo_url

def generate_pr_body(updates: list, skipped: list, cooldown_config: dict) -> str:
def generate_pr_body(updates: list, skipped: list, cooldown_config: dict, force_update: bool = False) -> str:
"""Generate comprehensive PR body."""
now = datetime.now(timezone.utc)

Expand Down Expand Up @@ -718,15 +713,24 @@ jobs:
lines.append("> Some updates have cooldown periods less than 7 days, which may increase vulnerability to supply chain attacks. Longer cooldown periods provide greater stability and more time to detect potential supply chain issues.")
lines.append("")

# Cooldown summary
lines.append("### Cooldown Periods Applied")
lines.append("")
lines.append(f"- **Major versions**: {cooldown_config['major']} days")
lines.append(f"- **Minor versions**: {cooldown_config['minor']} days")
lines.append(f"- **Patch versions**: {cooldown_config['patch']} days")
lines.append("")
# Cooldown summary (only show if not overridden by force_update)
if force_update:
lines.append("### Update Policy")
lines.append("")
lines.append("> [!NOTE]")
lines.append("> **Force Update Override**")
lines.append(">")
lines.append("> Cooldown periods were bypassed via `force_update: true`. All eligible updates were applied regardless of cooldown state.")
lines.append("")
else:
lines.append("### Cooldown Periods Applied")
lines.append("")
lines.append(f"- **Major versions**: {cooldown_config['major']} days")
lines.append(f"- **Minor versions**: {cooldown_config['minor']} days")
lines.append(f"- **Patch versions**: {cooldown_config['patch']} days")
lines.append("")

# Skipped updates
# Skipped updates (only relevant if not force_update)
if skipped:
lines.append("### Skipped Updates")
lines.append("")
Expand All @@ -740,10 +744,10 @@ jobs:
return "\n".join(lines)

# Generate PR body
pr_body = generate_pr_body(release_info, skipped, cooldown_config)
pr_body = generate_pr_body(release_info, skipped, cooldown_config, force_update)

# Create branch and commit
branch_name = f"chore/precommit-updates-{datetime.utcnow().strftime('%Y%m%d')}"
branch_name = f"chore/precommit-updates-{datetime.now(timezone.utc).strftime('%Y%m%d')}"

subprocess.run(['git', 'config', 'user.name', 'github-actions[bot]'], check=True)
subprocess.run(['git', 'config', 'user.email', '41898282+github-actions[bot]@users.noreply.github.com'], check=True)
Expand All @@ -753,8 +757,15 @@ jobs:
commit_msg = f"chore(pre-commit): auto-update hooks\n\nUpdated {len(release_info)} pre-commit hook(s)"
subprocess.run(['git', 'commit', '-m', commit_msg], check=True)

# Use GitHub CLI to authenticate git for pushing
# gh auth setup-git configures git to use gh's stored credentials
subprocess.run(['gh', 'auth', 'setup-git'], check=True)

# Push branch
subprocess.run(['git', 'push', '-u', 'origin', branch_name], check=True, env={**os.environ, 'GIT_TRACE': '1'})
subprocess.run(
['git', 'push', '--force-with-lease', '-u', 'origin', branch_name],
check=True
)

# Create PR using GitHub CLI
pr_result = subprocess.run(
Expand Down
20 changes: 10 additions & 10 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
repos:
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: 451b56af716f9f0d0c2b816503a3fd0cf8b036fa # frozen: v1.29.0
hooks:
- id: zizmor
args: [--fix, --persona=pedantic]
- repo: https://github.com/compilerla/conventional-pre-commit
rev: 3db014c16a9d31997ab8c07a4d61fcce936c8f0d # frozen: v4.4.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: fa412071e4f5d44d44f9e365f4676f9df92456a2
hooks:
- id: zizmor
args: [--fix, --persona=pedantic]
- repo: https://github.com/compilerla/conventional-pre-commit
rev: 3db014c16a9d31997ab8c07a4d61fcce936c8f0d # frozen: v4.4.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
10 changes: 5 additions & 5 deletions configs/precommit-update-tracking.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"last_updated": "2026-09-10T00:00:00Z",
"hooks": {
"https://github.com/zizmorcore/zizmor-pre-commit": {
"last_updated": "2026-09-10T00:00:00Z",
"current_sha": "451b56af716f9f0d0c2b816503a3fd0cf8b036fa",
"current_version": "v1.29.0",
"last_updated": "2026-09-10T11:54:29.403385+00:00Z",
"current_sha": "fa412071e4f5d44d44f9e365f4676f9df92456a2",
"current_version": "v1.30.1",
"semver_levels": {
"major": "2026-09-10T00:00:00Z",
"minor": "2026-09-10T00:00:00Z",
"minor": "2026-09-10T11:54:29.403370+00:00Z",
"patch": "2026-09-10T00:00:00Z"
}
},
Expand All @@ -22,4 +22,4 @@
}
}
}
}
}