From 3108eab336dc6a1b0bc83c97b29230234c69518b Mon Sep 17 00:00:00 2001 From: Brett Date: Fri, 10 Jul 2026 12:50:34 -0500 Subject: [PATCH 1/2] ci: drop dup push triggers and add concurrency caps ci.yml and skill-fixture-drift.yml fired on both push (main/dev) and pull_request, so every squash merge re-ran the same tree that the PR already verified. ci.yml drives rust-ci.yml's 6-job fan-out including a windows-latest leg billed at the 2x multiplier, so the duplicate roughly doubled cost on every merge. Both now trigger on pull_request only and carry a concurrency group keyed on workflow+ref so superseded pushes to an open PR cancel the in-flight run instead of queuing another one. skill-fixture-drift.yml's drift-check job also gets timeout-minutes now that it's confirmed not to be a reusable-workflow call. --- .github/workflows/ci.yml | 19 ++++++------------- .github/workflows/skill-fixture-drift.yml | 9 +++++++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4623322..06dd070 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,28 +5,21 @@ # Windows dep check, and changelog verification (on PRs to main). # No secrets required — only needs contents: read and pull-requests: read. # -# Push trigger is scoped to main/dev only — feature branches get CI via -# pull_request. This prevents duplicate runs when a branch push and PR -# creation fire seconds apart. tags-ignore prevents CI on release tag pushes -# (the release workflow handles those). paths-ignore skips doc-only pushes. -# paths-ignore is NOT applied to pull_request to avoid blocking required -# status checks when a PR only touches ignored paths. +# pull_request only: merges to main/dev are squash merges, so a push run +# would re-verify the identical tree already checked at the PR head. name: CI on: - push: - branches: [main, dev] - tags-ignore: ['**'] - paths-ignore: - - '**.md' - - 'LICENSE-*' - - 'docs/**' pull_request: permissions: contents: read pull-requests: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: ci: uses: brettdavies/.github/.github/workflows/rust-ci.yml@main diff --git a/.github/workflows/skill-fixture-drift.yml b/.github/workflows/skill-fixture-drift.yml index 679480d..38e1c92 100644 --- a/.github/workflows/skill-fixture-drift.yml +++ b/.github/workflows/skill-fixture-drift.yml @@ -9,17 +9,22 @@ name: skill-fixture-drift +# pull_request only: merges to main/dev are squash merges, so a push run +# would re-check the identical tree already checked at the PR head. on: pull_request: - push: - branches: [main, dev] permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: drift-check: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From 01392d6083301dda73aebf59ed0591a0c5a5b913 Mon Sep 17 00:00:00 2001 From: Brett Date: Fri, 10 Jul 2026 12:50:50 -0500 Subject: [PATCH 2/2] feat(hooks): add pre-commit gate for fast staged-file checks pre-push already mirrors CI's fmt/clippy/test/deny/shellcheck gates, but nothing ran at commit time, so a bad format or lint pass only surfaced at push. The new pre-commit hook scopes to staged files only so it stays fast: cargo fmt --check when .rs is staged, markdownlint when .md is staged, shellcheck when .sh is staged (including the hooks themselves). Each tool no-ops with a notice when not installed, same as pre-push. Also documents the two-hook activation line in the README Contributing section. --- README.md | 2 +- scripts/hooks/pre-commit | 75 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100755 scripts/hooks/pre-commit diff --git a/README.md b/README.md index 50720f9..e314594 100644 --- a/README.md +++ b/README.md @@ -404,7 +404,7 @@ Local setup: ```bash git clone https://github.com/brettdavies/agentnative-cli cd agentnative-cli -git config core.hooksPath scripts/hooks # mirror CI locally on every push +git config core.hooksPath scripts/hooks # fast checks on commit, full CI mirror on push cargo test cargo run -- audit . ``` diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100755 index 0000000..dad42a1 --- /dev/null +++ b/scripts/hooks/pre-commit @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Pre-commit hook: fast checks scoped to staged files only. +# +# Defensive: each tool no-ops with a notice when not installed, so it never +# blocks a commit on a machine that hasn't set up the full toolchain yet. +# Kept staged-file-scoped (not whole-repo) so it stays fast enough to run on +# every commit; the whole-repo equivalents live in scripts/hooks/pre-push. +# +# Activated per clone via: git config core.hooksPath scripts/hooks +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +BOLD='\033[1m' +RESET='\033[0m' + +pass() { echo -e " ${GREEN}✓${RESET} $1"; } +fail() { echo -e " ${RED}✗${RESET} $1"; exit 1; } + +echo -e "${BOLD}Running pre-commit checks...${RESET}" + +mapfile -t staged_files < <(git diff --cached --name-only --diff-filter=ACM) + +staged_rs=() +staged_md=() +staged_sh=() +for f in "${staged_files[@]}"; do + case "$f" in + *.rs) staged_rs+=("$f") ;; + *.md) staged_md+=("$f") ;; + *.sh) staged_sh+=("$f") ;; + esac +done +if printf '%s\n' "${staged_files[@]}" | grep -qx 'scripts/hooks/pre-commit\|scripts/hooks/pre-push'; then + staged_sh+=("scripts/hooks/pre-commit" "scripts/hooks/pre-push") +fi + +# 1. Rust formatting — staged .rs files only +if [ ${#staged_rs[@]} -gt 0 ]; then + if command -v cargo >/dev/null 2>&1; then + cargo fmt -- --check 2>/dev/null || fail "cargo fmt -- --check (run \`cargo fmt\` and re-stage)" + pass "fmt (${#staged_rs[@]} file(s))" + else + echo " - fmt (skipped: cargo not installed)" + fi +else + echo " - fmt (skipped: no staged .rs files)" +fi + +# 2. Markdown lint — staged .md files only +if [ ${#staged_md[@]} -gt 0 ]; then + if command -v markdownlint-cli2 >/dev/null 2>&1; then + markdownlint-cli2 "${staged_md[@]}" || fail "markdownlint-cli2" + pass "markdownlint (${#staged_md[@]} file(s))" + else + echo " - markdownlint (skipped: install via \`brew install markdownlint-cli2\`)" + fi +else + echo " - markdownlint (skipped: no staged .md files)" +fi + +# 3. Shellcheck — staged .sh files (and the hooks themselves if touched) +if [ ${#staged_sh[@]} -gt 0 ]; then + if command -v shellcheck >/dev/null 2>&1; then + mapfile -t staged_sh_uniq < <(printf '%s\n' "${staged_sh[@]}" | sort -u) + shellcheck --severity=warning "${staged_sh_uniq[@]}" || fail "shellcheck --severity=warning" + pass "shellcheck (${#staged_sh_uniq[@]} file(s))" + else + echo " - shellcheck (skipped: install via \`brew install shellcheck\`)" + fi +else + echo " - shellcheck (skipped: no staged .sh files)" +fi + +echo -e "${BOLD}${GREEN}Pre-commit checks passed.${RESET}"