Skip to content

Commit 6d921ac

Browse files
WomB0ComB0claude
andcommitted
chore: add dev tooling — hooks, skills, agents, codecov, dependabot
- Add .git-hooks/ (pre-commit, commit-msg, prepare-commit-msg, post-checkout, post-merge, pre-push) with resq-cli delegation, OSV scan, secrets scan, and dotnet format check - Add scripts/setup.sh with core.hooksPath wiring and Nix bootstrap - Add bootstrap.sh root alias for scripts/setup.sh - Add flake.nix devShell with osv-scanner and dotnet-sdk_9 - Add .github/skills/ (feynman-auditor, nemesis-auditor, state-inconsistency-auditor) - Add .github/agents/, .github/commands/, .github/rules/ for .NET context - Add .github/dependabot.yml with nuget + github-actions and minor/patch grouping - Add .github/workflows/auto-merge.yml for Dependabot PRs - Update .github/workflows/ci.yml with XPlat Code Coverage and Codecov upload - Add CLAUDE.md + AGENTS.md agent guide - Add agent-sync.sh to keep AGENTS.md and CLAUDE.md in sync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5d9b5a1 commit 6d921ac

File tree

33 files changed

+2627
-1
lines changed

33 files changed

+2627
-1
lines changed

.claude/agents

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../.github/agents

.claude/commands

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../.github/commands

.claude/rules

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../.github/rules

.claude/skills

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../.github/skills

.dockerignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
**/bin/
2+
**/obj/
3+
**/TestResults/
4+
**/.vs/
5+
.git/
6+
**/*.md
7+
artifacts/

.git-hooks/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Git Hooks — ResQ .NET SDK
2+
3+
This directory contains the project's git hooks. They enforce code quality, security, and workflow conventions for the ResQ .NET SDK (C#/.NET 9).
4+
5+
## Installation
6+
7+
```bash
8+
# Configure git to use these hooks
9+
git config core.hooksPath .git-hooks
10+
11+
# Or run the setup script (recommended)
12+
./scripts/setup.sh
13+
```
14+
15+
The setup script sets `core.hooksPath` to `.git-hooks` and makes all hooks executable.
16+
17+
## Active Hooks
18+
19+
| Hook | Purpose |
20+
|------|---------|
21+
| `pre-commit` | Large file guard (1 MB limit), secrets scan (gitleaks / grep fallback), C# formatting verification (`dotnet format --verify-no-changes`) |
22+
| `commit-msg` | Conventional Commits format validation; blocks `fixup!`/`squash!`/WIP on `main` |
23+
| `prepare-commit-msg` | Prepends ticket reference (e.g., `[PROJ-123]`) extracted from branch name |
24+
| `pre-push` | Force-push guard on `main`, branch naming convention, `dotnet build` check on changed `.cs` files |
25+
| `post-checkout` | Auto `dotnet restore` when `packages.lock.json` changes between branches |
26+
| `post-merge` | Auto `dotnet restore` when `packages.lock.json` changes after a merge |
27+
28+
## Bypassing Hooks
29+
30+
Use `--no-verify` to skip `pre-commit` and `commit-msg` hooks:
31+
32+
```bash
33+
git commit --no-verify -m "wip: quick save"
34+
git push --no-verify
35+
```
36+
37+
> **Note:** `post-checkout`, `post-merge`, and `prepare-commit-msg` cannot be bypassed with `--no-verify`.
38+
> Set `GIT_HOOKS_SKIP=1` to skip all custom hook logic.
39+
40+
## Environment Variables
41+
42+
| Variable | Effect | Scope |
43+
|----------|--------|-------|
44+
| `GIT_HOOKS_SKIP=1` | Skip all custom hook logic | `pre-commit`, `commit-msg`, `pre-push`, `post-checkout`, `post-merge` |
45+
46+
## Adding a New Hook
47+
48+
1. Create the hook file in `.git-hooks/` (no extension).
49+
2. Start with `#!/usr/bin/env bash` and add the Apache-2.0 license header.
50+
3. Make it executable: `chmod +x .git-hooks/<hook-name>`
51+
4. Test with: `bash -n .git-hooks/<hook-name>`
52+
5. Update this README with the new hook's purpose.

.git-hooks/commit-msg

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
#
4+
# Copyright 2026 ResQ
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# commit-msg
19+
#
20+
# Validates the commit message format.
21+
# Ensures the subject line follows the Conventional Commits specification.
22+
#
23+
# Usage:
24+
# .git-hooks/commit-msg COMMIT_MSG_FILE
25+
#
26+
# Arguments:
27+
# $1 - Path to the file containing the commit message.
28+
#
29+
# Exit codes:
30+
# 0 Commit message is valid.
31+
# 1 Commit message is invalid.
32+
33+
[ -n "${GIT_HOOKS_SKIP:-}" ] && exit 0
34+
35+
# INPUT_FILE stores the path to the commit message file.
36+
INPUT_FILE=${1:-}
37+
# PATTERN is the regular expression for a valid Conventional Commit message.
38+
PATTERN="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?: .+$"
39+
40+
# Validate only the first line (subject). Strip an optional [TICKET-123] prefix
41+
# inserted by prepare-commit-msg so the two hooks don't conflict.
42+
FIRST_LINE=$(head -1 "$INPUT_FILE")
43+
SUBJECT=$(echo "$FIRST_LINE" | sed 's/^\[[A-Z][A-Z]*-[0-9]*\] //')
44+
45+
if ! echo "$SUBJECT" | grep -qE "$PATTERN"; then
46+
echo "Error: Invalid commit message format."
47+
echo "Expected format: type(scope): subject"
48+
echo "Examples:"
49+
echo " feat(core): add new feature"
50+
echo " fix(ui): fix button color"
51+
exit 1
52+
fi
53+
54+
# Block fixup!/WIP commits on main
55+
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
56+
if [ "$BRANCH" = "main" ]; then
57+
if echo "$FIRST_LINE" | grep -qiE "^(\[[A-Z]+-[0-9]+\] )?(fixup!|squash!|wip[: ])"; then
58+
echo "Error: fixup!/squash!/WIP commits are not allowed on main."
59+
echo "Create a feature branch instead."
60+
exit 1
61+
fi
62+
fi

.git-hooks/post-checkout

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
#
4+
# Copyright 2026 ResQ
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# post-checkout
19+
#
20+
# Automatically installs dependencies when lock files change during branch checkout.
21+
#
22+
# Usage:
23+
# .git-hooks/post-checkout PREV_HEAD NEW_HEAD IS_BRANCH_CHECKOUT
24+
#
25+
# Arguments:
26+
# $1 - Previous HEAD commit hash.
27+
# $2 - New HEAD commit hash.
28+
# $3 - Flag indicating if it's a branch checkout (1) or file checkout (0).
29+
#
30+
# Exit codes:
31+
# 0 Always.
32+
33+
[ -n "${GIT_HOOKS_SKIP:-}" ] && exit 0
34+
35+
# PREV_HEAD stores the commit hash before checkout.
36+
PREV_HEAD="${1:-}"
37+
# NEW_HEAD stores the commit hash after checkout.
38+
NEW_HEAD="${2:-}"
39+
# IS_BRANCH_CHECKOUT is "1" if a branch was checked out, "0" otherwise.
40+
IS_BRANCH_CHECKOUT="${3:-}"
41+
42+
# Only run on branch checkouts, not file checkouts
43+
if [ "$IS_BRANCH_CHECKOUT" != "1" ]; then
44+
exit 0
45+
fi
46+
47+
# Skip if both heads are the same (no actual branch change)
48+
if [ "$PREV_HEAD" = "$NEW_HEAD" ]; then
49+
exit 0
50+
fi
51+
52+
# CHANGED stores the list of files that differ between the two heads.
53+
CHANGED=$(git diff --name-only "$PREV_HEAD" "$NEW_HEAD" 2>/dev/null)
54+
55+
# Check if packages.lock.json changed (NuGet lock file)
56+
if echo "$CHANGED" | grep -q "packages\.lock\.json"; then
57+
if command -v dotnet >/dev/null 2>&1; then
58+
echo "📦 packages.lock.json changed — running dotnet restore..."
59+
dotnet restore 2>/dev/null || true
60+
echo "✅ Dependencies restored"
61+
else
62+
echo "⚠️ dotnet not found — run 'dotnet restore' manually"
63+
fi
64+
fi

.git-hooks/post-merge

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
#
4+
# Copyright 2026 ResQ
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# post-merge
19+
#
20+
# Automatically installs dependencies when lock files change after a merge.
21+
#
22+
# Usage:
23+
# .git-hooks/post-merge
24+
#
25+
# Exit codes:
26+
# 0 Always.
27+
28+
[ -n "${GIT_HOOKS_SKIP:-}" ] && exit 0
29+
30+
# CHANGED_FILES stores the list of files changed in the merge.
31+
CHANGED_FILES=$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD 2>/dev/null)
32+
33+
# Check if packages.lock.json changed (NuGet lock file)
34+
if echo "$CHANGED_FILES" | grep -q "packages\.lock\.json"; then
35+
if command -v dotnet >/dev/null 2>&1; then
36+
echo "📦 packages.lock.json changed after merge — running dotnet restore..."
37+
dotnet restore 2>/dev/null || true
38+
echo "✅ Dependencies restored"
39+
else
40+
echo "⚠️ dotnet not found — run 'dotnet restore' manually"
41+
fi
42+
fi

.git-hooks/pre-commit

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
#
4+
# Copyright 2026 ResQ
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# pre-commit
19+
#
20+
# Runs checks before allowing a commit:
21+
# resq CLI (if installed) → secrets scan → OSV vuln scan → C# format check
22+
#
23+
# Install resq CLI: cargo install resq-cli
24+
#
25+
# Exit codes:
26+
# 0 All checks passed.
27+
# 1 Blocking check failed.
28+
29+
# ── Path resolution ───────────────────────────────────────────────────────────
30+
if [ ${#BASH_SOURCE[@]} -gt 0 ] && [ -n "${BASH_SOURCE[0]:-}" ]; then
31+
HOOK_PATH="${BASH_SOURCE[0]}"
32+
else
33+
HOOK_PATH="$0"
34+
fi
35+
HOOK_PATH=$(readlink -f "$HOOK_PATH")
36+
SCRIPT_DIR=$(dirname "$HOOK_PATH")
37+
PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
38+
39+
# ── Nix environment entry (if osv-scanner or dotnet missing) ──────────────────
40+
if [[ -z "${IN_NIX_SHELL:-}" ]] && [[ -z "${RESQ_NIX_GUARD:-}" ]]; then
41+
NEED_NIX=0
42+
for _tool in osv-scanner dotnet; do
43+
if ! command -v "$_tool" >/dev/null 2>&1; then NEED_NIX=1; break; fi
44+
done
45+
if [[ "$NEED_NIX" -eq 1 ]] && command -v nix >/dev/null 2>&1 && [ -f "$PROJECT_ROOT/flake.nix" ]; then
46+
echo "❄️ Entering Nix environment for pre-commit checks..."
47+
export RESQ_NIX_GUARD=1
48+
exec nix develop "$PROJECT_ROOT" --command bash "$HOOK_PATH" "$@"
49+
fi
50+
fi
51+
52+
# Source shared utilities if available
53+
if [ -f "$PROJECT_ROOT/scripts/lib/shell-utils.sh" ]; then
54+
source "$PROJECT_ROOT/scripts/lib/shell-utils.sh"
55+
fi
56+
57+
[ -n "${GIT_HOOKS_SKIP:-}" ] && exit 0
58+
59+
# ── Try resq CLI (handles all checks including OSV, secrets, formatting) ──────
60+
if command -v resq >/dev/null 2>&1; then
61+
exec resq pre-commit --root "$PROJECT_ROOT" "$@"
62+
fi
63+
echo "ℹ️ resq CLI not found — running inline checks. Install: cargo install resq-cli"
64+
65+
# ── Large file check ──────────────────────────────────────────────────────────
66+
echo "🔍 Checking staged file sizes..."
67+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
68+
for f in $STAGED_FILES; do
69+
if [ -f "$f" ]; then
70+
SIZE=$(wc -c < "$f")
71+
if [ "$SIZE" -gt 1048576 ]; then
72+
echo "❌ '$f' exceeds 1 MB. Large files must not be committed."
73+
exit 1
74+
fi
75+
fi
76+
done
77+
echo "✅ File sizes OK"
78+
79+
# ── Secrets scan ──────────────────────────────────────────────────────────────
80+
echo "🔍 Scanning for secrets..."
81+
if command -v gitleaks >/dev/null 2>&1; then
82+
if ! gitleaks protect --staged --redact -v 2>/dev/null; then
83+
echo "❌ Potential secrets detected. Remove them before committing."
84+
exit 1
85+
fi
86+
else
87+
STAGED_CONTENT=$(git diff --cached --unified=0 2>/dev/null || true)
88+
echo "$STAGED_CONTENT" | grep -qE 'AKIA[0-9A-Z]{16}' && { echo "❌ AWS key detected."; exit 1; } || true
89+
echo "$STAGED_CONTENT" | grep -qE '(ghp_|ghs_|gho_)[A-Za-z0-9_]{36,}' && { echo "❌ GitHub token detected."; exit 1; } || true
90+
echo "$STAGED_CONTENT" | grep -qF 'BEGIN PRIVATE KEY' && { echo "❌ Private key detected."; exit 1; } || true
91+
echo "$STAGED_CONTENT" | grep -qiE 'api[_-]?key\s*=\s*"[A-Za-z0-9_\-]{16,}"' && { echo "❌ API key detected."; exit 1; } || true
92+
echo "$STAGED_CONTENT" | grep -qiE 'password\s*=\s*"[^"]{4,}"' && { echo "❌ Password detected."; exit 1; } || true
93+
fi
94+
echo "✅ Secrets scan OK"
95+
96+
# ── OSV vulnerability scan ────────────────────────────────────────────────────
97+
if command -v osv-scanner >/dev/null 2>&1; then
98+
echo "🔍 Running OSV vulnerability scan..."
99+
OSV_ARGS=()
100+
[ -f "$PROJECT_ROOT/packages.lock.json" ] && OSV_ARGS+=(--lockfile "$PROJECT_ROOT/packages.lock.json")
101+
if [ ${#OSV_ARGS[@]} -gt 0 ]; then
102+
if ! osv-scanner scan "${OSV_ARGS[@]}" 2>/dev/null; then
103+
echo "❌ OSV scan found vulnerabilities. Run: osv-scanner scan --lockfile packages.lock.json"
104+
echo " To skip: GIT_HOOKS_SKIP=1 git commit"
105+
exit 1
106+
fi
107+
echo "✅ OSV scan OK"
108+
fi
109+
fi
110+
111+
# ── C# formatting check ───────────────────────────────────────────────────────
112+
STAGED_CS=$(git diff --cached --name-only --diff-filter=ACM | grep '\.cs$' || true)
113+
if [ -n "$STAGED_CS" ] && command -v dotnet >/dev/null 2>&1; then
114+
echo "🔍 Checking C# formatting..."
115+
if ! dotnet format --verify-no-changes --no-restore 2>/dev/null; then
116+
echo "❌ C# formatting issues. Run: dotnet format"
117+
exit 1
118+
fi
119+
echo "✅ C# formatting OK"
120+
fi
121+
122+
echo "✅ Pre-commit checks passed"

0 commit comments

Comments
 (0)