Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 10 additions & 6 deletions .github/workflows/hooks-tests.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
# 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.
# Run the bats suite over the canonical git hooks. The hook templates live
# in resq-software/crates; the suite fetches them at bats init time and
# tests behavior (here-string safety, local-* dispatch, stdin propagation,
# GIT_HOOKS_SKIP, etc).

name: hooks-tests

on:
push:
branches: [main]
paths:
- 'scripts/git-hooks/**'
- 'tests/hooks/**'
- 'scripts/install-hooks.sh'
- '.github/workflows/hooks-tests.yml'
pull_request:
paths:
- 'scripts/git-hooks/**'
- 'tests/hooks/**'
- 'scripts/install-hooks.sh'
- '.github/workflows/hooks-tests.yml'
# Weekly — catches canonical-template drift on the crates side that
# breaks bats assertions, without requiring a dev-side change to fire.
schedule:
- cron: '0 7 * * 1'
workflow_dispatch:

permissions:
Expand Down
81 changes: 57 additions & 24 deletions scripts/install-hooks.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
#
# Install canonical ResQ git hooks into a repository (PowerShell mirror).
#
# Usage (local — from dev/):
# Canonical hook content is owned by resq-software/crates. This installer:
# 1. Prefers `resq dev install-hooks` when the binary is on PATH (offline,
# scaffolds from embedded templates, versioned with the user's resq).
# 2. Falls back to fetching templates from crates raw.
#
# Usage (local):
# .\scripts\install-hooks.ps1 [-TargetDir <path>]
#
# Usage (curl-piped):
Expand All @@ -13,7 +18,7 @@
[CmdletBinding()]
param(
[string]$TargetDir = $PWD,
[string]$Ref = $(if ($env:RESQ_DEV_REF) { $env:RESQ_DEV_REF } else { 'main' })
[string]$Ref = $(if ($env:RESQ_CRATES_REF) { $env:RESQ_CRATES_REF } else { 'master' })
)

Set-StrictMode -Version Latest
Expand All @@ -27,40 +32,68 @@ if (-not $targetRoot) {
$hooksDir = Join-Path $targetRoot '.git-hooks'
if (-not (Test-Path $hooksDir)) { New-Item -ItemType Directory -Path $hooksDir -Force | Out-Null }

$hooks = @('pre-commit','commit-msg','prepare-commit-msg','pre-push','post-checkout','post-merge')

$scriptDir = if ($PSCommandPath) { Split-Path -Parent $PSCommandPath } else { $null }
$localSource = if ($scriptDir) { Join-Path $scriptDir 'git-hooks' } else { $null }
# ── Resolve resq binary ─────────────────────────────────────────────────────
$resqBin = $null
$onPath = Get-Command resq -ErrorAction SilentlyContinue
if ($onPath) {
$resqBin = 'resq'
} elseif (Test-Path (Join-Path $HOME '.cargo/bin/resq')) {
$resqBin = Join-Path $HOME '.cargo/bin/resq'
} elseif (Test-Path (Join-Path $HOME '.cargo/bin/resq.exe')) {
$resqBin = Join-Path $HOME '.cargo/bin/resq.exe'
}

if ($localSource -and (Test-Path $localSource)) {
Write-Host "info Installing hooks from $localSource" -ForegroundColor Cyan
foreach ($h in $hooks) {
Copy-Item (Join-Path $localSource $h) (Join-Path $hooksDir $h) -Force
}
# ── Path 1: use resq when present (preferred — offline, no raw fetch) ───────
if ($resqBin) {
Write-Host "info Installing hooks via $resqBin dev install-hooks" -ForegroundColor Cyan
Push-Location $targetRoot
try { & $resqBin dev install-hooks } finally { Pop-Location }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The script should check the exit code of the resq command to ensure the installation was successful, mirroring the set -e behavior in the shell version. If the command fails, the script currently continues and incorrectly reports success.

    try { & $resqBin dev install-hooks; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } finally { Pop-Location }

} else {
$rawBase = "https://raw.githubusercontent.com/resq-software/dev/$Ref/scripts/git-hooks"
# ── Path 2: fall back to raw fetch from crates templates ────────────────
$hooks = @('pre-commit','commit-msg','prepare-commit-msg','pre-push','post-checkout','post-merge')
$rawBase = "https://raw.githubusercontent.com/resq-software/crates/$Ref/crates/resq-cli/templates/git-hooks"
Write-Host "info Fetching hooks from $rawBase" -ForegroundColor Cyan
foreach ($h in $hooks) {
Invoke-WebRequest -Uri "$rawBase/$h" -OutFile (Join-Path $hooksDir $h) -UseBasicParsing
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

By default, Invoke-WebRequest does not throw a terminating error on HTTP failures (like 404). This can lead to a partial or corrupted installation where hook files contain error messages instead of script content. Adding -ErrorAction Stop ensures the script fails fast on network or server errors.

        Invoke-WebRequest -Uri "$rawBase/$h" -OutFile (Join-Path $hooksDir $h) -UseBasicParsing -ErrorAction Stop

}
if ($IsLinux -or $IsMacOS) {
foreach ($h in $hooks) { & chmod +x (Join-Path $hooksDir $h) }
}
& git -C $targetRoot config core.hooksPath .git-hooks
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The exit code of the git config command should be checked to ensure the repository was correctly configured to use the new hooks directory.

    & git -C $targetRoot config core.hooksPath .git-hooks
    if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

}

# chmod +x on non-Windows.
if ($IsLinux -or $IsMacOS) {
foreach ($h in $hooks) { & chmod +x (Join-Path $hooksDir $h) }
}

& git -C $targetRoot config core.hooksPath .git-hooks

Write-Host " ok ResQ hooks installed in $hooksDir" -ForegroundColor Green
Write-Host " Bypass once: git commit --no-verify"
Write-Host " Disable all hooks: `$env:GIT_HOOKS_SKIP = '1'"
Write-Host " Add repo logic: $hooksDir/local-<hook-name>"

$hasResq = ($null -ne (Get-Command resq -ErrorAction SilentlyContinue)) -or
(Test-Path (Join-Path $HOME '.cargo/bin/resq'))
if (-not $hasResq) {
if (-not $resqBin) {
Write-Host "warn resq backend not found. Hooks will soft-skip until you install it:" -ForegroundColor Yellow
Write-Host " nix develop (if your flake provides it)"
Write-Host " cargo install --git https://github.com/resq-software/crates resq-cli"
Write-Host " irm https://raw.githubusercontent.com/resq-software/dev/main/scripts/install-resq.sh | sh"
Write-Host " (or) cargo install --git https://github.com/resq-software/crates resq-cli"
exit 0
}

# ── Local-hook scaffold prompt ──────────────────────────────────────────────
if ((Test-Path (Join-Path $hooksDir 'local-pre-push')) -or $env:RESQ_SKIP_LOCAL_SCAFFOLD) { exit 0 }

# Probe for subcommand support.
$probe = & $resqBin dev scaffold-local-hook --help 2>&1
if ($LASTEXITCODE -ne 0) { exit 0 }

$answer = ''
if ($env:YES -eq '1') {
$answer = 'y'
} elseif ([Environment]::UserInteractive) {
$answer = Read-Host 'info Scaffold a repo-specific local-pre-push (auto-detect kind)? [y/N]'
}

if ($answer -match '^[yY]') {
Push-Location $targetRoot
try {
& $resqBin dev scaffold-local-hook --kind auto
if ($LASTEXITCODE -ne 0) {
Write-Host 'warn scaffold-local-hook failed; run it manually with --kind <name>.' -ForegroundColor Yellow
}
} finally { Pop-Location }
}
Loading