Skip to content

Conversation

@ichoosetoaccept
Copy link

Add a new builtin hook that checks if configured hooks have newer versions available.

Features

  • --cooldown-days: minimum release age (in days) for a version to be eligible (default: 0)
  • --fail-on-updates: fail the hook if updates are available (default: warn only)

Example usage

repos:
  - repo: builtin
    hooks:
      - id: check-hook-updates
        args: [--cooldown-days=3]

Changes

  • Added CheckHookUpdates variant to BuiltinHooks enum
  • Implemented hook in pre_commit_hooks/check_hook_updates.rs
  • Added tests
  • Updated documentation
  • Added hook to project's own .pre-commit-config.yaml

Closes #1243

@codecov
Copy link

codecov bot commented Dec 20, 2025

Codecov Report

❌ Patch coverage is 82.03593% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.96%. Comparing base (2bb96c5) to head (6b3673c).
⚠️ Report is 7 commits behind head on master.

Files with missing lines Patch % Lines
...prek/src/hooks/builtin_hooks/check_hook_updates.rs 73.83% 28 Missing ⚠️
crates/prek/src/cli/auto_update.rs 95.34% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1249      +/-   ##
==========================================
- Coverage   91.06%   90.96%   -0.10%     
==========================================
  Files          87       88       +1     
  Lines       18384    18516     +132     
==========================================
+ Hits        16741    16843     +102     
- Misses       1643     1673      +30     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link

github-actions bot commented Dec 20, 2025

📦 Cargo Bloat Comparison

Binary size change: +0.44% (22.8 MiB → 22.9 MiB)

Expand for cargo-bloat output

Head Branch Results

 File  .text    Size        Crate Name
 0.3%   0.8% 73.9KiB        prek? <prek::cli::Command as clap_builder::derive::Subcommand>::augment_subcommands
 0.3%   0.7% 62.5KiB         prek prek::languages::<impl prek::config::Language>::run::{{closure}}::{{closure}}
 0.3%   0.7% 61.8KiB         prek prek::languages::<impl prek::config::Language>::run::{{closure}}::{{closure}}
 0.2%   0.5% 46.6KiB         prek prek::run::{{closure}}
 0.2%   0.5% 43.8KiB         prek prek::cli::run::run::run::{{closure}}
 0.2%   0.5% 42.8KiB         prek prek::languages::<impl prek::config::Language>::install::{{closure}}
 0.2%   0.5% 42.7KiB         prek prek::identify::by_extension::{{closure}}
 0.1%   0.3% 31.8KiB        prek? <prek::cli::RunArgs as clap_builder::derive::Args>::augment_args
 0.1%   0.3% 30.0KiB  serde_json? <&mut serde_json::de::Deserializer<R> as serde_core::de::Deserializer>::deserialize_struct
 0.1%   0.2% 22.2KiB         prek prek::hooks::meta_hooks::MetaHooks::run::{{closure}}
 0.1%   0.2% 22.0KiB         prek prek::hooks::meta_hooks::MetaHooks::run::{{closure}}
 0.1%   0.2% 21.3KiB clap_builder clap_builder::parser::parser::Parser::get_matches_with
 0.1%   0.2% 21.1KiB         prek prek::archive::unzip::{{closure}}
 0.1%   0.2% 20.2KiB         prek prek::cli::run::filter::collect_files_from_args::{{closure}}
 0.1%   0.2% 19.6KiB         prek prek::cli::run::filter::collect_files_from_args::{{closure}}
 0.1%   0.2% 19.6KiB         prek <prek::languages::ruby::ruby::Ruby as prek::languages::LanguageImpl>::install::{{closure}}
 0.1%   0.2% 18.8KiB         prek prek::hook::HookBuilder::build::{{closure}}
 0.1%   0.2% 18.8KiB         prek prek::hook::HookBuilder::build::{{closure}}
 0.1%   0.2% 18.8KiB         prek prek::hook::HookBuilder::build::{{closure}}
 0.1%   0.2% 18.6KiB         ring ring_core_0_17_14__x25519_ge_frombytes_vartime
36.9%  91.4%  8.4MiB              And 20141 smaller methods. Use -n N to show more.
40.4% 100.0%  9.2MiB              .text section size, the file size is 22.9MiB

Base Branch Results

 File  .text    Size        Crate Name
 0.3%   0.8% 72.2KiB        prek? <prek::cli::Command as clap_builder::derive::Subcommand>::augment_subcommands
 0.3%   0.7% 61.5KiB         prek prek::languages::<impl prek::config::Language>::run::{{closure}}::{{closure}}
 0.2%   0.6% 56.4KiB         prek prek::languages::<impl prek::config::Language>::run::{{closure}}::{{closure}}
 0.2%   0.5% 46.3KiB         prek prek::run::{{closure}}
 0.2%   0.5% 43.8KiB         prek prek::cli::run::run::run::{{closure}}
 0.2%   0.5% 42.8KiB         prek prek::languages::<impl prek::config::Language>::install::{{closure}}
 0.2%   0.5% 42.7KiB         prek prek::identify::by_extension::{{closure}}
 0.1%   0.3% 32.0KiB        prek? <prek::cli::RunArgs as clap_builder::derive::Args>::augment_args
 0.1%   0.3% 30.0KiB  serde_json? <&mut serde_json::de::Deserializer<R> as serde_core::de::Deserializer>::deserialize_struct
 0.1%   0.2% 22.2KiB         prek prek::hooks::meta_hooks::MetaHooks::run::{{closure}}
 0.1%   0.2% 22.0KiB         prek prek::hooks::meta_hooks::MetaHooks::run::{{closure}}
 0.1%   0.2% 21.5KiB         prek prek::archive::unzip::{{closure}}
 0.1%   0.2% 21.3KiB clap_builder clap_builder::parser::parser::Parser::get_matches_with
 0.1%   0.2% 20.0KiB         prek prek::cli::run::filter::collect_files_from_args::{{closure}}
 0.1%   0.2% 19.6KiB         prek prek::cli::run::filter::collect_files_from_args::{{closure}}
 0.1%   0.2% 19.6KiB         prek <prek::languages::ruby::ruby::Ruby as prek::languages::LanguageImpl>::install::{{closure}}
 0.1%   0.2% 18.8KiB         prek prek::hook::HookBuilder::build::{{closure}}
 0.1%   0.2% 18.8KiB         prek prek::hook::HookBuilder::build::{{closure}}
 0.1%   0.2% 18.8KiB         prek prek::hook::HookBuilder::build::{{closure}}
 0.1%   0.2% 18.6KiB         ring ring_core_0_17_14__x25519_ge_frombytes_vartime
36.9%  91.5%  8.4MiB              And 20103 smaller methods. Use -n N to show more.
40.3% 100.0%  9.2MiB              .text section size, the file size is 22.8MiB

@j178
Copy link
Owner

j178 commented Dec 20, 2025

Is it possible to eliminate the code duplication from auto_update.rs?

@ichoosetoaccept
Copy link
Author

Is it possible to eliminate the code duplication from auto_update.rs?

Good call. Done!

@ichoosetoaccept
Copy link
Author

Apologies - my previous commit didn't actually eliminate the duplication as I claimed. I've now properly extracted the shared logic into reusable functions in auto_update.rs:

check_hook_updates.rs now imports and reuses these instead of duplicating the logic.

Comment on lines +2121 to +2328
fn check_hook_updates_hook_recognized() {
let context = TestContext::new();
context.init_project();

context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: builtin
hooks:
- id: check-hook-updates
"});
context.git_add(".");

// The hook should be recognized and run (it will pass since there are no remote repos to check)
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
check for hook updates...................................................Passed
----- stderr -----
");
}

/// Tests that `check-hook-updates` hook with `--fail-on-updates` argument is recognized.
#[test]
fn check_hook_updates_hook_with_args() {
let context = TestContext::new();
context.init_project();

context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: builtin
hooks:
- id: check-hook-updates
args: ['--cooldown-days=7', '--fail-on-updates']
"});
context.git_add(".");

// The hook should be recognized and run with the arguments
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
check for hook updates...................................................Passed
----- stderr -----
");
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think these two tests are enough, because there aren’t any remote repos in the config file, so the update logic never actually runs. Right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You, sir, are correct again.

Just added tests that exercise the update-checking logic with a real remote repo (pre-commit/pre-commit-hooks v4.0.0):

  • check_hook_updates_detects_outdated_repo: verifies hook runs and detects updates
  • check_hook_updates_fails_on_updates: verifies --fail-on-updates correctly fails when updates are available

@pygarap
Copy link

pygarap commented Jan 4, 2026

@ichoosetoaccept

  1. Can it actually update versions like prek auto-update, or does it only check for updates?
  2. If you have a .pre-commit-config.yaml with a different name and run prek run --config, how does this hook behave?

@ichoosetoaccept
Copy link
Author

@pygarap
To answer your questions:

  1. It only checks for updates and reports them—it does not modify the config file. The hook is intentionally read-only: it warns (or fails with --fail-on-updates) when newer versions are available, but leaves the actual updating to prek auto-update.
    This design follows the principle that hooks should be predictable and non-destructive by default. Auto-updating during a commit could be surprising and potentially break CI pipelines.
  2. It works correctly. The hook accesses its config via hook.project().config(), which is already resolved to the correct config file path when prek run --config <custom-config.yaml> is invoked. The hook doesn't hardcode any config file path—it uses whatever config the parent project was initialized with.

@j178 j178 added the enhancement New feature or request label Jan 13, 2026
@ulgens
Copy link
Contributor

ulgens commented Jan 14, 2026

When would this hook run? I tried to understand the "files" arg but couldn't see it in the diff, and I believe it will run everytime prek is triggered. This could cause the hook to run when not needed and generate noise. I wouldn't want this hook to complain about my git hook setup when the only thing I'm doing is editing an unrelated code file.

@ichoosetoaccept
Copy link
Author

ichoosetoaccept commented Jan 19, 2026

When would this hook run? I tried to understand the "files" arg but couldn't see it in the diff, and I believe it will run everytime prek is triggered. This could cause the hook to run when not needed and generate noise. I wouldn't want this hook to complain about my git hook setup when the only thing I'm doing is editing an unrelated code file.

@ulgens Great point! I've addressed this by adding a check interval feature that throttles how often the hook actually runs.

Changes

New default behavior:

  • The hook now only performs the actual update check at most once every 24 hours (configurable via --check-interval-hours)
  • The last check timestamp is stored in the prek cache directory
  • Additionally, --cooldown-days now defaults to 7 days (was 0) to avoid suggesting brand-new releases that haven't been vetted by the community yet (supply chain security)

New arguments:

  • --check-interval-hours=<N> (default: 24) — minimum hours between checks. Set to 0 to check every time (useful for CI)
  • --cooldown-days=<N> (default: 7) — minimum release age before suggesting an update

Does this address your concerns adequately or do you have better ideas/further suggestions?

Example usage

repos:
  - repo: builtin
    hooks:
      - id: check-hook-updates
        # Check at most once per day, only suggest releases older than 7 days
        # (these are the defaults, shown for clarity)
        args: ['--check-interval-hours=24', '--cooldown-days=7']

For CI where you want every-run checks:

      - id: check-hook-updates
        args: ['--check-interval-hours=0', '--fail-on-updates']

Open question

One thing I noticed: when the check is skipped due to the interval, the hook still shows "Passed" rather than "Skipped". This could be slightly misleading since no actual check occurred.

Would it be better to show "Skipped" in this case? I'd need to look into how the hook result reporting works to see if this is feasible without larger changes. Open to suggestions!

@ulgens
Copy link
Contributor

ulgens commented Jan 19, 2026

The hook now only performs the actual update check at most once every 24 hours (configurable via --check-interval-hours)

I don't have a strong opposition to this implementation, and one could easily not enable the hook if they don't like it, but it feels a lot like this should be a step/layer in the CI , not a git hook. git hooks are generally (always?) invoked based on the repository state and files, not a pre-defined interval. In contrast, running a weekly task on CI feels much more natural.

@ichoosetoaccept
Copy link
Author

That's a fair point! You're right that git hooks are traditionally file/state-based, and a time-based interval is unconventional.

A few thoughts on why this might still be useful as a hook:

  1. Discoverability — Many teams don't have CI set up for every repo, or contributors work on personal forks without CI. Having it as a hook means it "just works" for everyone who clones the repo.

  2. Opt-in flexibility — Users can:

    • Not enable the hook at all
    • Set --check-interval-hours=0 for CI-like behavior (check every time)
    • Use stages: [manual] to only run on demand (prek run --hook-stage manual)
  3. Complementary to CI — This doesn't replace a weekly CI job; it supplements it by catching outdated hooks during local development before they become a CI failure.

That said, I'm open to alternative approaches. If the time-based behavior feels too unconventional, users can simply set stages: [manual] when adding the hook—that way it only runs on demand via prek run --hook-stage manual. The hook isn't enabled by default; users choose to add it, so they're already making an explicit decision about when it should run.

Ismar Iljazovic added 3 commits January 25, 2026 02:08
Add a new builtin hook that checks if configured hooks have newer
versions available. Supports:
- `--cooldown-days`: minimum release age for eligible updates
- `--fail-on-updates`: fail instead of just warning

Closes j178#1243
…te duplication

Extract shared functionality from auto_update.rs that check_hook_updates
can reuse:

- `find_eligible_tag`: cooldown-based tag selection with best candidate logic
- `resolve_rev_to_commit_hash`: dereference a revision to its commit hash

This eliminates duplicated code for:
- Cooldown calculation and binary search for eligible tags
- Git rev-parse logic for resolving revisions to commit hashes
Add tests that actually exercise the update-checking logic by using
a real remote repo (pre-commit/pre-commit-hooks) with an outdated
version (v4.0.0).

- check_hook_updates_detects_outdated_repo: verifies hook runs with remote repos
- check_hook_updates_fails_on_updates: verifies --fail-on-updates works
- Default to checking at most once every 24 hours to avoid slowing commits
- Store last check timestamp in prek cache directory
- Change cooldown-days default from 0 to 7 for supply chain security
- Add --check-interval-hours=0 to force check every time (useful for CI)
- Update docs with new arguments and security rationale
- Add tests for check interval behavior

Addresses review feedback about hook running on every commit.
@j178 j178 force-pushed the check-hook-updates branch from 37908ff to 6b3673c Compare January 27, 2026 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: repo: builtin hook to check for outdated hook versions

5 participants