diff --git a/.github/workflows/auto-update-precommit-hooks.yml b/.github/workflows/auto-update-precommit-hooks.yml index 0863d4a..d456e63 100644 --- a/.github/workflows/auto-update-precommit-hooks.yml +++ b/.github/workflows/auto-update-precommit-hooks.yml @@ -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" @@ -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: @@ -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: @@ -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, @@ -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: @@ -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 @@ -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" @@ -539,19 +512,20 @@ 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' }} @@ -559,14 +533,15 @@ jobs: 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']), @@ -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} @@ -606,9 +585,9 @@ jobs: '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: @@ -616,13 +595,13 @@ jobs: 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) @@ -635,7 +614,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) @@ -718,15 +697,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("") @@ -740,10 +728,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) @@ -753,8 +741,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( diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb454fb..6aab591 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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] \ No newline at end of file +- repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: fa412071e4f5d44d44f9e365f4676f9df92456a2 # 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] diff --git a/configs/precommit-update-tracking.json b/configs/precommit-update-tracking.json index 6cb4bfd..400a94e 100644 --- a/configs/precommit-update-tracking.json +++ b/configs/precommit-update-tracking.json @@ -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:24:42.825334+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:24:42.825323+00:00Z", "patch": "2026-09-10T00:00:00Z" } }, @@ -22,4 +22,4 @@ } } } -} +} \ No newline at end of file