-
Notifications
You must be signed in to change notification settings - Fork 0
test(hooks): bats suite (33 tests) + CI workflow + 2 hook bugfixes #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # Copyright 2026 ResQ Software | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Run the bats suite over the canonical git-hooks shipped from this repo. | ||
| # | ||
| # Decoupled from hooks-sync.yml on purpose so this file can ship in a | ||
| # different PR without merge conflicts. | ||
|
|
||
| name: hooks-tests | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'scripts/git-hooks/**' | ||
| - 'tests/hooks/**' | ||
| - '.github/workflows/hooks-tests.yml' | ||
| pull_request: | ||
| paths: | ||
| - 'scripts/git-hooks/**' | ||
| - 'tests/hooks/**' | ||
| - '.github/workflows/hooks-tests.yml' | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| bats: | ||
| name: bats | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Install bats | ||
| run: | | ||
| sudo apt-get update -y | ||
| sudo apt-get install -y bats | ||
|
|
||
| - name: Run hook test suite | ||
| run: bats tests/hooks/ | ||
|
|
||
| - name: Print bats version (diagnostic) | ||
| if: always() | ||
| run: bats --version |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| #!/usr/bin/env bats | ||
| # Conventional Commits validation in the canonical commit-msg hook. | ||
|
|
||
| load helpers | ||
|
|
||
| setup() { | ||
| REPO="$(mktemp -d)" | ||
| init_repo_with_hooks "$REPO" | ||
| MSG="$REPO/.msg" | ||
| } | ||
|
|
||
| teardown() { | ||
| rm -rf "$REPO" | ||
| } | ||
|
|
||
| write_msg() { printf '%s\n' "$1" > "$MSG"; } | ||
|
|
||
| @test "accepts feat: subject" { | ||
| write_msg "feat: add the thing" | ||
| run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -eq 0 ] | ||
| } | ||
|
|
||
| @test "accepts feat(scope): subject" { | ||
| write_msg "feat(api): add /v2/foo" | ||
| run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -eq 0 ] | ||
| } | ||
|
|
||
| @test "accepts feat!: breaking change marker" { | ||
| write_msg "feat!: drop legacy endpoint" | ||
| run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -eq 0 ] | ||
| } | ||
|
|
||
| @test "accepts feat(scope)!: breaking with scope" { | ||
| write_msg "feat(api)!: drop legacy endpoint" | ||
| run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -eq 0 ] | ||
| } | ||
|
|
||
| @test "accepts ticket-prefixed message" { | ||
| write_msg "[ABC-123] feat: add ticket prefix" | ||
| run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -eq 0 ] | ||
| } | ||
|
|
||
| @test "rejects unknown type" { | ||
| write_msg "wat: this is not a type" | ||
| run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -ne 0 ] | ||
| [[ "$output" == *"Invalid commit message"* ]] | ||
| } | ||
|
|
||
| @test "rejects missing subject after type" { | ||
| write_msg "feat:" | ||
| run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -ne 0 ] | ||
| } | ||
|
|
||
| @test "rejects WIP on main" { | ||
| checkout_branch "$REPO" main | ||
| write_msg "WIP: still working" | ||
| run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -ne 0 ] | ||
| [[ "$output" == *"not allowed on main"* ]] | ||
| } | ||
|
|
||
| @test "rejects fixup! on master" { | ||
| checkout_branch "$REPO" master | ||
| write_msg "fixup! feat: thing" | ||
| run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -ne 0 ] | ||
| } | ||
|
|
||
| @test "GIT_HOOKS_SKIP=1 short-circuits even on bad input" { | ||
| write_msg "garbage that would normally fail" | ||
| GIT_HOOKS_SKIP=1 run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -eq 0 ] | ||
| } | ||
|
|
||
| @test "subject starting with - does not break here-string handling" { | ||
| # Branch where head: was an `echo $X | grep` ate `-` as flag | ||
| write_msg "feat: -fix typo in flag handling" | ||
| run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -eq 0 ] | ||
| } | ||
|
|
||
| @test "dispatches to local-commit-msg when present and executable" { | ||
| cat > "$REPO/.git-hooks/local-commit-msg" <<'EOF' | ||
| #!/usr/bin/env bash | ||
| echo "LOCAL_FIRED" | ||
| EOF | ||
| chmod +x "$REPO/.git-hooks/local-commit-msg" | ||
| write_msg "feat: trigger local" | ||
| run run_hook "$REPO" commit-msg "$MSG" | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" == *"LOCAL_FIRED"* ]] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # shellcheck shell=bash | ||
| # Common helpers for bats tests over the canonical ResQ git hooks. | ||
| # | ||
| # Each test gets a fresh tempdir initialized as a git repo with the canonical | ||
| # hooks copied in. core.hooksPath is set so `git` invokes the hooks naturally. | ||
|
|
||
| # Absolute path to the canonical hook templates shipped by this repo. | ||
| HOOK_SRC="${BATS_TEST_DIRNAME}/../../scripts/git-hooks" | ||
|
|
||
| # Initialize a fresh git repo in $1 with canonical hooks installed. | ||
| init_repo_with_hooks() { | ||
| local dir="$1" | ||
| git -C "$dir" init -q | ||
| git -C "$dir" -c user.email=t@t.io -c user.name=t commit --allow-empty -m "init" -q | ||
| mkdir -p "$dir/.git-hooks" | ||
| cp "$HOOK_SRC"/{pre-commit,commit-msg,prepare-commit-msg,pre-push,post-checkout,post-merge} "$dir/.git-hooks/" | ||
| chmod +x "$dir/.git-hooks"/* | ||
| git -C "$dir" config core.hooksPath .git-hooks | ||
| git -C "$dir" config user.email t@t.io | ||
| git -C "$dir" config user.name t | ||
| } | ||
|
|
||
| # Run a hook directly against a repo: run_hook <repo-dir> <hook-name> [args...] | ||
| run_hook() { | ||
| local dir="$1" hook="$2" | ||
| shift 2 | ||
| (cd "$dir" && bash ".git-hooks/$hook" "$@") | ||
| } | ||
|
|
||
| # Force-switch to branch <name> (creates if missing). Avoids the fragile | ||
| # `branch -m` || `checkout -b` dance in tests. | ||
| checkout_branch() { | ||
| local dir="$1" name="$2" | ||
| git -C "$dir" checkout -q -B "$name" | ||
| git -C "$dir" symbolic-ref HEAD "refs/heads/$name" | ||
| } | ||
|
|
||
| # Make a setup commit without firing the installed hooks. | ||
| # Args: <dir> <message> [extra git-commit args...] | ||
| commit_no_hooks() { | ||
| local dir="$1" msg="$2" | ||
| shift 2 | ||
| git -C "$dir" -c "core.hooksPath=" commit --allow-empty -q -m "$msg" "$@" | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| #!/usr/bin/env bats | ||
| # Lock-file change notices and local dispatch for post-checkout/post-merge. | ||
|
|
||
| load helpers | ||
|
|
||
| setup() { | ||
| REPO="$(mktemp -d)" | ||
| init_repo_with_hooks "$REPO" | ||
| } | ||
|
|
||
| teardown() { | ||
| rm -rf "$REPO" | ||
| } | ||
|
|
||
| @test "post-checkout notifies on Cargo.lock change" { | ||
| PREV=$(git -C "$REPO" rev-parse HEAD) | ||
| printf 'lock\n' > "$REPO/Cargo.lock" | ||
| git -C "$REPO" add Cargo.lock | ||
| git -C "$REPO" -c "core.hooksPath=" commit -q -m "feat: lock" | ||
| NEW=$(git -C "$REPO" rev-parse HEAD) | ||
| run run_hook "$REPO" post-checkout "$PREV" "$NEW" 1 | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" == *"Cargo.lock changed"* ]] | ||
| } | ||
|
|
||
| @test "post-checkout silent when no lockfile changed" { | ||
| PREV=$(git -C "$REPO" rev-parse HEAD) | ||
| git -C "$REPO" commit --allow-empty -q -m "feat: nothing" | ||
| NEW=$(git -C "$REPO" rev-parse HEAD) | ||
| run run_hook "$REPO" post-checkout "$PREV" "$NEW" 1 | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" != *"changed"* ]] | ||
| } | ||
|
|
||
| @test "post-checkout dispatches to local-post-checkout" { | ||
| cat > "$REPO/.git-hooks/local-post-checkout" <<'EOF' | ||
| #!/usr/bin/env bash | ||
| echo "LOCAL_POST_CHECKOUT_RAN" | ||
| EOF | ||
| chmod +x "$REPO/.git-hooks/local-post-checkout" | ||
| PREV=$(git -C "$REPO" rev-parse HEAD) | ||
| commit_no_hooks "$REPO" "x" | ||
| NEW=$(git -C "$REPO" rev-parse HEAD) | ||
| run run_hook "$REPO" post-checkout "$PREV" "$NEW" 1 | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" == *"LOCAL_POST_CHECKOUT_RAN"* ]] | ||
| } | ||
|
|
||
| @test "GIT_HOOKS_SKIP=1 short-circuits post-checkout" { | ||
| cat > "$REPO/.git-hooks/local-post-checkout" <<'EOF' | ||
| #!/usr/bin/env bash | ||
| echo "SHOULD_NOT_RUN" | ||
| EOF | ||
| chmod +x "$REPO/.git-hooks/local-post-checkout" | ||
| PREV=$(git -C "$REPO" rev-parse HEAD) | ||
| commit_no_hooks "$REPO" "x" | ||
| NEW=$(git -C "$REPO" rev-parse HEAD) | ||
| GIT_HOOKS_SKIP=1 run run_hook "$REPO" post-checkout "$PREV" "$NEW" 1 | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" != *"SHOULD_NOT_RUN"* ]] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| #!/usr/bin/env bats | ||
| # Soft-skip + local dispatch behavior in the canonical pre-commit hook. | ||
| # (The full `resq pre-commit` checks are exercised by resq-cli's own tests; | ||
| # here we verify wiring only.) | ||
|
|
||
| load helpers | ||
|
|
||
| setup() { | ||
| REPO="$(mktemp -d)" | ||
| init_repo_with_hooks "$REPO" | ||
| } | ||
|
|
||
| teardown() { | ||
| rm -rf "$REPO" | ||
| } | ||
|
|
||
| @test "soft-skip with hint when resq is missing" { | ||
| # Run with PATH that excludes any resq + override $HOME to a clean dir | ||
| # so ~/.cargo/bin/resq isn't found either. | ||
| EMPTY="$(mktemp -d)" | ||
| PATH="/usr/bin:/bin" HOME="$EMPTY" run bash -c "cd '$REPO' && bash .git-hooks/pre-commit" | ||
| rm -rf "$EMPTY" | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" == *"resq not found"* ]] | ||
| } | ||
|
|
||
| @test "GIT_HOOKS_SKIP=1 short-circuits before the resq lookup" { | ||
| EMPTY="$(mktemp -d)" | ||
| GIT_HOOKS_SKIP=1 PATH="/usr/bin:/bin" HOME="$EMPTY" run bash -c "cd '$REPO' && bash .git-hooks/pre-commit" | ||
| rm -rf "$EMPTY" | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" != *"resq not found"* ]] | ||
| } | ||
|
|
||
| @test "dispatches to local-pre-commit even when resq is missing" { | ||
| cat > "$REPO/.git-hooks/local-pre-commit" <<'EOF' | ||
| #!/usr/bin/env bash | ||
| echo "LOCAL_PRE_COMMIT_RAN" | ||
| EOF | ||
| chmod +x "$REPO/.git-hooks/local-pre-commit" | ||
| EMPTY="$(mktemp -d)" | ||
| PATH="/usr/bin:/bin" HOME="$EMPTY" run bash -c "cd '$REPO' && bash .git-hooks/pre-commit" | ||
| rm -rf "$EMPTY" | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" == *"LOCAL_PRE_COMMIT_RAN"* ]] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| #!/usr/bin/env bats | ||
| # Branch naming, force-push guard, stdin propagation in the canonical pre-push hook. | ||
|
|
||
| load helpers | ||
|
|
||
| setup() { | ||
| REPO="$(mktemp -d)" | ||
| init_repo_with_hooks "$REPO" | ||
| } | ||
|
|
||
| teardown() { | ||
| rm -rf "$REPO" | ||
| } | ||
|
|
||
| # Generates a fake "git push" stdin line for one ref. | ||
| # Args: local_ref local_sha remote_ref remote_sha | ||
| push_line() { printf '%s %s %s %s\n' "$1" "$2" "$3" "$4"; } | ||
|
|
||
| @test "accepts feat/ branch name" { | ||
| git -C "$REPO" checkout -q -b feat/add-thing | ||
| run bash -c "cd '$REPO' && bash .git-hooks/pre-push origin git@example </dev/null" | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" == *"Pre-push checks passed"* ]] | ||
| } | ||
|
|
||
| @test "rejects bad branch name" { | ||
| git -C "$REPO" checkout -q -b nope/bad-prefix | ||
| run bash -c "cd '$REPO' && bash .git-hooks/pre-push origin git@example </dev/null" | ||
| [ "$status" -ne 0 ] | ||
| [[ "$output" == *"does not follow naming convention"* ]] | ||
| } | ||
|
|
||
| @test "skips check on main" { | ||
| checkout_branch "$REPO" main | ||
| run bash -c "cd '$REPO' && bash .git-hooks/pre-push origin git@example </dev/null" | ||
| [ "$status" -eq 0 ] | ||
| } | ||
|
|
||
| @test "skips check on changeset-release/* branches" { | ||
| git -C "$REPO" checkout -q -b changeset-release/main | ||
| run bash -c "cd '$REPO' && bash .git-hooks/pre-push origin git@example </dev/null" | ||
| [ "$status" -eq 0 ] | ||
| } | ||
|
|
||
| @test "GIT_HOOKS_SKIP=1 short-circuits" { | ||
| git -C "$REPO" checkout -q -b nope/bad | ||
| GIT_HOOKS_SKIP=1 run bash -c "cd '$REPO' && bash .git-hooks/pre-push origin git@example </dev/null" | ||
| [ "$status" -eq 0 ] | ||
| } | ||
|
|
||
| @test "local-pre-push receives the push refs on stdin" { | ||
| git -C "$REPO" checkout -q -b feat/x | ||
| cat > "$REPO/.git-hooks/local-pre-push" <<'EOF' | ||
| #!/usr/bin/env bash | ||
| # Echo what's on stdin so the test can verify propagation. | ||
| echo "LOCAL_STDIN_BEGIN" | ||
| cat | ||
| echo "LOCAL_STDIN_END" | ||
| EOF | ||
| chmod +x "$REPO/.git-hooks/local-pre-push" | ||
|
|
||
| LINE="$(push_line refs/heads/feat/x abc1234 refs/heads/feat/x def5678)" | ||
| run bash -c "cd '$REPO' && printf '%s\n' '$LINE' | bash .git-hooks/pre-push origin git@example" | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" == *"LOCAL_STDIN_BEGIN"* ]] | ||
| [[ "$output" == *"$LINE"* ]] | ||
| [[ "$output" == *"LOCAL_STDIN_END"* ]] | ||
| } | ||
|
|
||
| @test "force push to main is rejected" { | ||
| checkout_branch "$REPO" main | ||
| # local sha (HEAD) and a fake remote sha that isn't an ancestor → force push. | ||
| LOCAL=$(git -C "$REPO" rev-parse HEAD) | ||
| REMOTE="0000000000000000000000000000000000000001" # arbitrary non-zero sha | ||
| LINE="$(push_line refs/heads/main "$LOCAL" refs/heads/main "$REMOTE")" | ||
| run bash -c "cd '$REPO' && printf '%s\n' '$LINE' | bash .git-hooks/pre-push origin git@example" | ||
| [ "$status" -ne 0 ] | ||
| [[ "$output" == *"Force push"* ]] | ||
| } | ||
|
|
||
| @test "fast-forward push to main is allowed" { | ||
| checkout_branch "$REPO" main | ||
| OLD=$(git -C "$REPO" rev-parse HEAD) | ||
| commit_no_hooks "$REPO" "feat: more" | ||
| NEW=$(git -C "$REPO" rev-parse HEAD) | ||
| LINE="$(push_line refs/heads/main "$NEW" refs/heads/main "$OLD")" | ||
| run bash -c "cd '$REPO' && printf '%s\n' '$LINE' | bash .git-hooks/pre-push origin git@example" | ||
| [ "$status" -eq 0 ] | ||
| } | ||
|
|
||
| @test "branch starting with - does not break grep here-string handling" { | ||
| # `git checkout -b -foo` is rejected by git itself, so we target the | ||
| # naming-convention check only — the regex must handle a `-` safely. | ||
| # Use a name that begins with a normal prefix but contains tricky chars. | ||
| git -C "$REPO" checkout -q -b "feat/-leading-dash" | ||
| run bash -c "cd '$REPO' && bash .git-hooks/pre-push origin git@example </dev/null" | ||
| [ "$status" -eq 0 ] | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
cpcommand includespost-mergein the brace expansion, but this file is not present in thescripts/git-hooks/directory according to the repository structure. This will cause theinit_repo_with_hookshelper to fail, subsequently breaking all tests that rely on it.