Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 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
63f52a0
feat: add ADR-0006 for stateful pre-commit update automation
ColinDaglish Sep 17, 2026
af60bf5
fix: update dependency installation to use requirements.txt
ColinDaglish Sep 17, 2026
d3f9460
fix: remove 'Z' suffix from ISO format timestamps in pre-commit updat…
ColinDaglish Sep 17, 2026
0878973
fix: update comment and command for fetching latest release tag in au…
ColinDaglish Sep 17, 2026
cd95f5f
feat: add pre-commit updates automation package
ColinDaglish Sep 18, 2026
2cbf86d
fix: enhance permissions comments for clarity in auto-update workflow
ColinDaglish Sep 18, 2026
613dab3
feat: add temp skip-check configuration to expose workflow_dispatch c…
ColinDaglish Sep 18, 2026
0c3be94
Update .pre-commit-config.yaml
ColinDaglish Sep 18, 2026
96113f0
Delete configs/precommit-update-tracking.json
ColinDaglish Sep 18, 2026
d4c84dd
feat: add validation for pre-commit configuration and tracking alignment
ColinDaglish Sep 18, 2026
fb906bc
feat: enhance reporting for pre-commit updates and add summary writin…
ColinDaglish Sep 18, 2026
b2694f8
Merge branch 'datasciencecampus:update-pre-commit-hooks' into update-…
ColinDaglish Sep 18, 2026
8c59121
chore(pre-commit): update zizmor-pre-commit to v1.30.1
github-actions[bot] Sep 18, 2026
e99e589
chore(pre-commit): update conventional-pre-commit to v4.4.0
github-actions[bot] Sep 18, 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
674 changes: 46 additions & 628 deletions .github/workflows/auto-update-precommit-hooks.yml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.pyc
__pycache__/
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 # frozen: v1.30.1
hooks:
- id: zizmor
args: [--fix, --persona=pedantic]
- repo: https://github.com/compilerla/conventional-pre-commit
rev: 91ab4bf57e58b32adf1a122681f6ebe164d081c8 # frozen: v4.4.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
4 changes: 3 additions & 1 deletion configs/checkov.yml
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
{}
skip-check:
# This workflow intentionally exposes workflow_dispatch controls for update policy.
- CKV_GHA_7
26 changes: 11 additions & 15 deletions configs/precommit-update-tracking.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
{
"last_updated": "2026-09-10T00:00:00Z",
"last_updated": "2026-09-18T09:06:17.752603+00:00",
"hooks": {
"https://github.com/zizmorcore/zizmor-pre-commit": {
"last_updated": "2026-09-10T00:00:00Z",
"current_sha": "451b56af716f9f0d0c2b816503a3fd0cf8b036fa",
"current_version": "v1.29.0",
"current_sha": "fa412071e4f5d44d44f9e365f4676f9df92456a2",
"semver_levels": {
"major": "2026-09-10T00:00:00Z",
"minor": "2026-09-10T00:00:00Z",
"patch": "2026-09-10T00:00:00Z"
}
"minor": "2026-09-18T09:06:17.741932+00:00"
},
"current_version": "v1.30.1",
"last_updated": "2026-09-18T09:06:17.741932+00:00"
},
"https://github.com/compilerla/conventional-pre-commit": {
"last_updated": "2026-09-10T00:00:00Z",
"current_sha": "3db014c16a9d31997ab8c07a4d61fcce936c8f0d",
"current_version": "v4.4.0",
"current_sha": "91ab4bf57e58b32adf1a122681f6ebe164d081c8",
"semver_levels": {
"major": "2026-09-10T00:00:00Z",
"minor": "2026-09-10T00:00:00Z",
"patch": "2026-09-10T00:00:00Z"
}
"minor": "2026-09-18T09:06:17.752603+00:00"
},
"current_version": "v4.4.0",
"last_updated": "2026-09-18T09:06:17.752603+00:00"
}
}
}
82 changes: 82 additions & 0 deletions docs/explanation/ADR-0006-stateful-precommit-update-automation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# ADR-0006: Stateful pre-commit update automation

- Status: Accepted
- Date: 2026-09-17

## Context

We needed to automate updates to the commit-pinned pre-commit hooks used by this repository without turning upstream release activity into an automatic change to the default branch. The workflow must discover new releases, provide enough release context for review, remember update history, and create a pull request for a human to approve.

This introduces an architectural trust boundary: most workflow stages only inspect repository contents and public upstream metadata, while one final stage can write repository contents and create pull requests. It also introduces state because cooldown decisions cannot be made reliably from the current `.pre-commit-config.yaml` alone.

## Decision

Implement the automation as a stateful, staged workflow in `auto-update-precommit-hooks.yml`:

1. **Detect updates** from tagged upstream releases and resolve each release tag to an immutable commit SHA. Only hooks with tagged releases are considered.
2. **Apply policy** using semver-level cooldowns, the committed tracking file, the configured skip list, and an explicit manual `force_update` override.
3. **Fetch release context** such as release notes and commit information for updates that passed policy.
4. **Write and propose changes** in a single job that updates `.pre-commit-config.yaml` and `configs/precommit-update-tracking.json`, pushes a bot branch, and creates a pull request. The default branch is changed only through the normal pull request review and merge process.

The workflow uses `contents: read` for detection, filtering, and release-context jobs. Only the final job receives `contents: write` and `pull-requests: write`. Actions use pinned commit SHAs, checkout does not persist credentials, and the GitHub token is passed only to the steps that need it.

## Rationale

1. **Release trust and reproducibility**
Release tags are used as the human-readable update signal, but the configuration is updated to the resolved commit SHA. This preserves reviewable release information while preventing a mutable tag from changing the code consumed by pre-commit.

2. **Cooldowns reduce supply-chain exposure**
A newly published release is not adopted immediately by default. Major updates wait 28 days, minor updates 14 days, and patch updates 7 days. The graduated periods reflect increasing compatibility risk while allowing security and bug-fix updates to move sooner. The waiting period also provides time for upstream issues or malicious releases to become visible.

3. **Tracking state makes policy durable**
`configs/precommit-update-tracking.json` records the current SHA, current version, last update, and last update time for each semver level. Committing this state in the same pull request as the hook change makes cooldown decisions reproducible across scheduled runs and auditable in Git history.

4. **Pull requests preserve human control**
The workflow creates a pull request containing release notes, commit information, cooldown policy, and risk warnings. It does not merge the change. Reviewers remain responsible for deciding whether an upstream release is suitable for the repository.

5. **Force updates remain explicit**
`force_update` is available only as an explicit workflow input and is documented as a supply-chain risk. Keeping the normal path subject to cooldowns makes the secure behavior the default while preserving an operational escape hatch for urgent fixes.

6. **Write access is isolated**
Separating read-only discovery from the write-enabled PR job limits the impact of failures or compromised data in upstream metadata. The write boundary is easy to audit and is reached only after the update has passed filtering and release-context collection.

## Consequences

Positive:

- Hook updates are commit-pinned, reviewable, and traceable to upstream releases.
- Scheduled runs can make consistent cooldown decisions using committed state.
- The default branch is protected by the existing pull request review process.
- Read-only jobs do not need write-capable credentials.
- Release notes and risk information are available to reviewers in the generated pull request.

Negative:

- The tracking file is additional repository state that must remain consistent with `.pre-commit-config.yaml`.
- Cooldowns delay adoption of some fixes and require an explicit override for urgent updates.
- The workflow is more complex than a direct `pre-commit autoupdate` job because it must resolve releases, preserve state, and construct a reviewable pull request.
- The generated pull request still requires human review and merge, so automation cannot guarantee that hooks are always current.

## Alternatives considered

1. **Update the default branch directly**
- Pro: No pull request queue or manual merge step
- Con: Removes the review gate for third-party code and would give scheduled automation direct write authority over the default branch

2. **Use mutable release tags in `.pre-commit-config.yaml`**
- Pro: Simpler configuration and readable diffs
- Con: A tag can be retargeted after review, so the consumed hook would not be reproducible

3. **Use only the current configuration as state**
- Pro: No tracking file to maintain
- Con: There is no durable record of when each semver level was last adopted, making cooldown enforcement unreliable across runs

4. **Adopt every available release immediately**
- Pro: Fastest access to upstream fixes
- Con: Increases exposure to compromised or defective releases before they have had time to receive scrutiny

## Related decisions

- ADR-0001: Called workflow owns secret usage
- ADR-0002: Use workflow_dispatch instead of repository_dispatch
- ADR-0004: Separate reusable workflow pinning from dispatch ref
1 change: 1 addition & 0 deletions docs/explanation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ Background, rationale, and trade-offs.
- [ADR-0003: Unified project_field_values input with optional field updates](ADR-0003-unified-project-field-values-input.md)
- [ADR-0004: Separate reusable workflow pinning from dispatch ref](ADR-0004-separate-reusable-pinning-from-dispatch-ref.md)
- [ADR-0005: Security workflow orchestration pattern](ADR-0005-security-workflow-orchestration.md)
- [ADR-0006: Stateful pre-commit update automation](ADR-0006-stateful-precommit-update-automation.md)
23 changes: 23 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "precommit-updates"
version = "0.1.0"
description = "Testable automation for commit-pinned pre-commit hook updates"
requires-python = ">=3.11"
dependencies = [
"PyYAML==6.0.3",
"ruamel.yaml==0.19.1",
]

[project.scripts]
precommit-updates = "precommit_updates.cli:main"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
PyYAML==6.0.3
ruamel.yaml==0.19.1
61 changes: 61 additions & 0 deletions src/precommit_updates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Pre-Commit Updates

`precommit_updates` contains the Python implementation used by the
`auto-update-precommit-hooks` GitHub Actions workflow.

The package keeps update policy and file mutation testable while leaving job
sequencing, permissions, and secrets in the workflow.

## CLI stages

Run the module from the repository root with `PYTHONPATH=src`:

```shell
PYTHONPATH=src python3 -m precommit_updates detect
PYTHONPATH=src python3 -m precommit_updates cooldown
PYTHONPATH=src python3 -m precommit_updates release-info
PYTHONPATH=src python3 -m precommit_updates apply
```

The stages correspond to the workflow jobs:

- `detect` finds newer tagged releases and resolves them to commit SHAs.
- `cooldown` applies skip-list, force-update, and semver cooldown policy.
- `release-info` adds release notes and commits from the requested comparison range.
- `apply` updates the YAML and tracking files, creates one commit per hook, pushes
the branch, and creates the pull request.

Each stage accepts optional paths:

```shell
PYTHONPATH=src python3 -m precommit_updates detect \
--config .pre-commit-config.yaml \
--tracking configs/precommit-update-tracking.json
```

## Workflow contracts

The CLI reads and writes the following GitHub Actions environment values:

- `UPDATES_JSON` for the cooldown stage
- `ELIGIBLE_JSON` for the release-info stage
- `RELEASE_INFO` and `SKIPPED_UPDATES` for the apply stage
- `COOLDOWN_MAJOR`, `COOLDOWN_MINOR`, and `COOLDOWN_PATCH`
- `FORCE_UPDATE` and `SKIP_HOOKS`

When `GITHUB_OUTPUT` is set, stage output is written using the existing workflow
keys: `updates_found`, `updates_json`, `eligible_updates`, `skipped_updates`,
and `release_info`.

## Development

Install the runtime dependencies from the repository root and run the focused
unit tests:

```shell
pip install -r requirements.txt
python -m pytest tests/unit -q
```

The GitHub client is designed for mocked tests. Normal tests do not require
GitHub credentials or network access.
1 change: 1 addition & 0 deletions src/precommit_updates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Pre-commit hook update automation."""
7 changes: 7 additions & 0 deletions src/precommit_updates/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Run the pre-commit update CLI as a Python module."""

from .cli import main


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