-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathpre-commit.sh
More file actions
executable file
·118 lines (99 loc) · 5.19 KB
/
Copy pathpre-commit.sh
File metadata and controls
executable file
·118 lines (99 loc) · 5.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#!/usr/bin/env bash
#
# Iris pre-commit quality gate. Symlinked to .git/hooks/pre-commit.
# Order: safety -> format -> lint -> types -> tests -> summary.
set -uo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT" || exit 1
fail=0
note() { printf "%b\n" "$1"; }
step() { printf "\n%b==> %s%b\n" "$YELLOW" "$1" "$NC"; }
# Staged files (added/copied/modified) as a newline string — portable (no mapfile,
# works on macOS bash 3.2). Each grep over it tolerates an empty list.
STAGED="$(git diff --cached --name-only --diff-filter=ACM)"
staged() { printf '%s\n' "$STAGED"; }
ts_staged() { staged | grep -E '\.(ts|tsx)$' || true; }
# ----- 1. SAFETY -----------------------------------------------------------
step "Safety checks"
# 1a. plan/ never ships
if staged | grep -qE '^plan/'; then
note "${RED}✗ plan/ files are staged — research never ships${NC}"; fail=1
fi
# 1b. .env never committed (only .env.example)
if staged | grep -E '(^|/)\.env($|\.)' | grep -qv '\.env\.example'; then
note "${RED}✗ a .env file is staged${NC}"; fail=1
fi
# 1c. obvious secrets in the added lines
if git diff --cached -U0 2>/dev/null \
| grep -E '^\+' \
| grep -Eiq '(api[_-]?key|secret|password|private[_-]?key)["'"'"']?[[:space:]]*[:=][[:space:]]*["'"'"'][A-Za-z0-9/_+-]{12,}'; then
note "${RED}✗ possible hardcoded secret in staged changes${NC}"; fail=1
fi
# 1d. no `any`, no console.log, file size, bare eslint-disable
while IFS= read -r f; do
[ -z "$f" ] && continue
[ -f "$f" ] || continue
if grep -nE '(:[[:space:]]*any\b|<any>|as any\b|any\[\])' "$f" >/dev/null; then
note "${RED}✗ 'any' type in $f${NC}"; fail=1
fi
if grep -nE '\bconsole\.log\b' "$f" >/dev/null; then
note "${RED}✗ console.log in $f (use console.warn/error or structured logging)${NC}"; fail=1
fi
lines=$(wc -l < "$f" | tr -d ' ')
if [ "$lines" -gt 500 ]; then
note "${RED}✗ $f is $lines lines (> 500 cap) — split it${NC}"; fail=1
fi
if grep -nE 'eslint-disable(-next-line|-line)?' "$f" | grep -vq -- '--'; then
note "${RED}✗ eslint-disable without a '-- reason' in $f${NC}"; fail=1
fi
done < <(ts_staged)
# 1e. component files must use create-, not new-
if staged | grep -E '(components|views|features)/' | grep -qE '/new-[^/]+\.(ts|tsx)$'; then
note "${RED}✗ component file prefixed 'new-' — use 'create-'${NC}"; fail=1
fi
# 1f. no internal tracking tags in source files
# Rejects: design-doc codes (G4, N5, M8, P2, F1, R1, …) and version labels (0.3.7) in comments.
# Matches lines that start with a comment marker (// or #) and contain a bare letter+digit token
# or a semver-like string. Skips the pre-commit.sh itself and skills/ docs (those explain the rule).
while IFS= read -r f; do
[ -z "$f" ] && continue
[ -f "$f" ] || continue
case "$f" in pre-commit.sh|skills/*) continue;; esac
if grep -nE '(//|#)[^"'"'"'`]*\b[A-Z][0-9]+(\.[0-9]+)?\b' "$f" >/dev/null 2>&1; then
note "${RED}✗ internal tracking tag (e.g. N5, G4, M8) in comment in $f — use prose instead${NC}"; fail=1
fi
if grep -nE '(//|#)[^"'"'"'`]*\b[0-9]+\.[0-9]+\.[0-9]+\b' "$f" >/dev/null 2>&1; then
note "${RED}✗ version string in comment in $f — remove internal milestone labels${NC}"; fail=1
fi
done < <(ts_staged)
[ "$fail" -eq 0 ] && note "${GREEN}✓ safety${NC}"
# Steps 2-7 mirror CI (.github/workflows/ci.yml) one-for-one so a green commit is a green CI run:
# build -> format -> lint -> types -> tests -> audit. Keep this list in sync with ci.yml.
# ----- 2. BUILD ------------------------------------------------------------
step "Build (turbo)"
if ! pnpm -s build; then note "${RED}✗ build${NC}"; fail=1; else note "${GREEN}✓ build${NC}"; fi
# ----- 3. FORMAT -----------------------------------------------------------
step "Prettier (format check)"
if ! pnpm -s format:check; then note "${RED}✗ prettier${NC}"; fail=1; else note "${GREEN}✓ format${NC}"; fi
# ----- 4. LINT -------------------------------------------------------------
step "ESLint"
if ! pnpm -s lint; then note "${RED}✗ eslint${NC}"; fail=1; else note "${GREEN}✓ lint${NC}"; fi
# ----- 5. TYPES ------------------------------------------------------------
step "TypeScript (tsc --build)"
if ! pnpm -s typecheck; then note "${RED}✗ types${NC}"; fail=1; else note "${GREEN}✓ types${NC}"; fi
# ----- 6. TESTS ------------------------------------------------------------
step "Unit tests (vitest)"
if ! pnpm -s test:unit; then note "${RED}✗ tests${NC}"; fail=1; else note "${GREEN}✓ tests${NC}"; fi
# ----- 7. AUDIT ------------------------------------------------------------
# Same gate as CI. Needs network (queries the advisory DB); an offline commit will fail here — that
# is the intended CI parity, so a high+ vuln is caught before push, not after.
step "Security audit (--audit-level high)"
if ! pnpm audit --audit-level high; then note "${RED}✗ audit (high+ vulnerability)${NC}"; fail=1; else note "${GREEN}✓ audit${NC}"; fi
# ----- 8. SUMMARY ----------------------------------------------------------
if [ "$fail" -ne 0 ]; then
note "\n${RED}✗ pre-commit FAILED — commit blocked${NC}"
exit 1
fi
note "\n${GREEN}✓ all checks passed${NC}"
exit 0