Skip to content

Commit 62a5085

Browse files
Merge pull request #24 from offendingcommit/chore/local-prepush-gates
chore(hooks): local pre-push gate + secret scan
2 parents a26b028 + 86bb9d6 commit 62a5085

6 files changed

Lines changed: 202 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ dist-ssr
4141
# Tauri
4242
packages/desktop/src-tauri/target/
4343
packages/desktop/src-tauri/gen/
44+
45+
# Drafts from scripts/pr-evidence.sh
46+
PR_BODY.md

.husky/pre-commit

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
#!/bin/sh
2+
# Pre-commit gates — must be fast (<2s typical). Anything slow goes in pre-push.
3+
#
4+
# Order:
5+
# 1. Secret scan (must run first; blocks commit if a secret leaks in)
6+
# 2. Biome format/lint on staged TS/JS/JSON/CSS files
7+
8+
# 1. Secret scan over staged additions
9+
./scripts/secret-scan.sh
10+
11+
# 2. Biome on staged files (auto-fixes, re-stages)
212
STAGED=$(git diff --cached --name-only --diff-filter=ACMR | grep -E "\.(ts|tsx|js|jsx|css|json)$" || true)
313
[ -z "$STAGED" ] && exit 0
414
pnpm exec biome check --write --staged

.husky/pre-push

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
# Pre-push gate — runs the full quality check before the branch leaves the
3+
# laptop. Mirrors the `check` job in .github/workflows/ci.yml.
4+
#
5+
# Bypass for genuine emergencies with: git push --no-verify
6+
7+
echo "→ Running pre-push checks (lint + typecheck + test)…"
8+
pnpm check

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"check": "turbo run lint typecheck test",
2323
"ci:web": "turbo run lint typecheck test build --filter=@openconcho/web",
2424
"ci:desktop": "turbo run cargo-check --filter=@openconcho/desktop",
25+
"pr:evidence": "./scripts/pr-evidence.sh",
2526
"prepare": "husky"
2627
},
2728
"devDependencies": {

scripts/pr-evidence.sh

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env bash
2+
# Draft a PR body for the current branch based on the diff vs origin/main.
3+
#
4+
# Writes to PR_BODY.md in the repo root (gitignored — see end of script).
5+
# Pre-fills the structure required by .github/pull_request_template.md
6+
# and flags whether screenshots are required based on touched paths.
7+
#
8+
# Usage:
9+
# ./scripts/pr-evidence.sh # writes ./PR_BODY.md
10+
# ./scripts/pr-evidence.sh > /tmp/body.md # write to stdout
11+
12+
set -euo pipefail
13+
14+
OUTPUT="${1:-PR_BODY.md}"
15+
16+
# Find the base branch (default origin/main) the current branch diverged from.
17+
BASE_REF="${BASE_REF:-origin/main}"
18+
git fetch origin main --quiet 2>/dev/null || true
19+
20+
MERGE_BASE=$(git merge-base HEAD "$BASE_REF" 2>/dev/null || echo "$BASE_REF")
21+
CHANGED=$(git diff --name-only "$MERGE_BASE"...HEAD)
22+
ADDED=$(git diff --name-status --diff-filter=A "$MERGE_BASE"...HEAD | awk '{print $2}')
23+
MODIFIED=$(git diff --name-status --diff-filter=M "$MERGE_BASE"...HEAD | awk '{print $2}')
24+
DELETED=$(git diff --name-status --diff-filter=D "$MERGE_BASE"...HEAD | awk '{print $2}')
25+
26+
# Heuristic: any touched path under packages/web/src/{components,routes} or
27+
# packages/desktop counts as a UI change and requires screenshots.
28+
UI_CHANGED=0
29+
if echo "$CHANGED" | grep -qE '^(packages/web/src/(components|routes)|packages/desktop)/'; then
30+
UI_CHANGED=1
31+
fi
32+
33+
# Commits since base — useful for the "What" section.
34+
COMMITS=$(git log --pretty=format:'- %s' "$MERGE_BASE"..HEAD)
35+
36+
# Tests touched?
37+
TESTS_TOUCHED=$(echo "$CHANGED" | grep -E '(\.test\.|/test/|/e2e/)' || true)
38+
39+
BRANCH=$(git rev-parse --abbrev-ref HEAD)
40+
41+
draft() {
42+
cat <<EOF
43+
<!--
44+
Auto-drafted by scripts/pr-evidence.sh from the diff vs ${BASE_REF}.
45+
Fill in the prose sections; the file lists and checklist are pre-populated.
46+
-->
47+
48+
## Why
49+
50+
<!-- The problem this solves, in 1–3 sentences. What pain or gap does this close? -->
51+
52+
## What
53+
54+
$(if [ -n "$COMMITS" ]; then printf 'Commits on this branch:\n%s\n' "$COMMITS"; else echo '<!-- describe the change -->'; fi)
55+
56+
$(if [ -n "$ADDED" ]; then printf '\n**Added:**\n'; printf '%s\n' "$ADDED" | sed 's/^/- /'; fi)
57+
$(if [ -n "$MODIFIED" ]; then printf '\n**Modified:**\n'; printf '%s\n' "$MODIFIED" | sed 's/^/- /'; fi)
58+
$(if [ -n "$DELETED" ]; then printf '\n**Deleted:**\n'; printf '%s\n' "$DELETED" | sed 's/^/- /'; fi)
59+
60+
## Screenshots
61+
62+
EOF
63+
64+
if [ $UI_CHANGED -eq 1 ]; then
65+
cat <<EOF
66+
**Required** — this PR touches packages/web/src/{components,routes} or packages/desktop.
67+
Commit screenshots under \`docs/screenshots/<feature-slug>/\` and reference here:
68+
69+
\`\`\`markdown
70+
![Description](https://raw.githubusercontent.com/BenSheridanEdwards/openconcho/${BRANCH}/docs/screenshots/<feature-slug>/01-<state>.png)
71+
\`\`\`
72+
73+
See \`.claude/rules/workflows.md\` → "Open a PR" for capture + commit guidance.
74+
75+
EOF
76+
else
77+
cat <<EOF
78+
<!-- No packages/web/src/{components,routes} or packages/desktop paths touched —
79+
screenshots not strictly required. Delete this section if truly docs-only. -->
80+
81+
EOF
82+
fi
83+
84+
cat <<EOF
85+
## QA checklist
86+
87+
- [ ] \`pnpm typecheck\` clean locally
88+
- [ ] \`pnpm lint\` clean locally
89+
- [ ] \`pnpm test\` green locally
90+
$(if [ -n "$TESTS_TOUCHED" ]; then echo '- [x] Tests touched on this branch:'; printf '%s\n' "$TESTS_TOUCHED" | sed 's/^/ - /'; else echo '- [ ] Tests added for new behaviour (or note why none are needed)'; fi)
91+
- [ ] Manual verification: <!-- which Honcho instance, which workspace/peer, what you clicked, what you saw -->
92+
$(if echo "$CHANGED" | grep -qE '^packages/desktop/'; then echo '- [ ] \`pnpm --filter @openconcho/desktop cargo-check\` passes'; fi)
93+
- [x] Worked in a git worktree (current branch: \`${BRANCH}\`)
94+
95+
## Out-of-scope
96+
97+
<!-- What was intentionally left out and why. -->
98+
99+
## Notes
100+
101+
<!-- Caveats, follow-ups, anything reviewers should know. -->
102+
EOF
103+
}
104+
105+
if [ "$OUTPUT" = "-" ] || [ -t 1 ]; then
106+
# When piped or first arg is "-", write to stdout.
107+
if [ "${1:-}" = "-" ]; then
108+
draft
109+
exit 0
110+
fi
111+
fi
112+
113+
draft > "$OUTPUT"
114+
115+
echo "✓ Drafted PR body → ${OUTPUT}"
116+
echo " Open it, fill in Why / Manual verification / Out-of-scope / Notes, then use as the PR body."

scripts/secret-scan.sh

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
# Secret scan for staged files.
3+
#
4+
# Pre-commit hook calls this against staged additions. Fast (no external
5+
# tool; just regex over the staged diff). Designed to catch the common
6+
# accidents — API keys committed alongside code — not to replace a full
7+
# secret-scanning service.
8+
#
9+
# Exits non-zero with a clear message if a likely secret is found.
10+
11+
set -euo pipefail
12+
13+
# Only scan added/modified content (the `+` lines in the staged diff).
14+
# This avoids false positives from existing committed strings.
15+
STAGED_DIFF=$(git diff --cached --diff-filter=ACMR --unified=0 -- '*.ts' '*.tsx' '*.js' '*.jsx' '*.json' '*.yml' '*.yaml' '*.toml' '*.env*' '*.sh' '*.md' 2>/dev/null || true)
16+
17+
if [ -z "$STAGED_DIFF" ]; then
18+
exit 0
19+
fi
20+
21+
# Only look at added lines (starting with `+`, excluding diff headers `+++`).
22+
ADDED=$(printf '%s\n' "$STAGED_DIFF" | grep -E '^\+[^+]' || true)
23+
24+
if [ -z "$ADDED" ]; then
25+
exit 0
26+
fi
27+
28+
FOUND=0
29+
FINDINGS=""
30+
31+
check_pattern() {
32+
local name="$1"
33+
local pattern="$2"
34+
# Use `-e` to safely pass patterns that begin with `-` (e.g. PEM headers).
35+
if printf '%s\n' "$ADDED" | grep -qE -e "$pattern"; then
36+
FOUND=1
37+
FINDINGS="${FINDINGS} - ${name}\n"
38+
fi
39+
}
40+
41+
check_pattern "AWS access key" 'AKIA[0-9A-Z]{16}'
42+
check_pattern "AWS secret key (high-entropy)" 'aws_secret_access_key[[:space:]]*[:=][[:space:]]*[A-Za-z0-9/+=]{40}'
43+
check_pattern "Anthropic API key" 'sk-ant-[a-zA-Z0-9_-]{32,}'
44+
check_pattern "OpenAI API key" 'sk-[a-zA-Z0-9]{20,}T3BlbkFJ[a-zA-Z0-9]{20,}'
45+
check_pattern "OpenAI project key (newer)" 'sk-proj-[a-zA-Z0-9_-]{40,}'
46+
check_pattern "GitHub personal access token" 'gh[psoru]_[A-Za-z0-9_]{36,}'
47+
check_pattern "GitHub fine-grained PAT" 'github_pat_[A-Za-z0-9_]{82,}'
48+
check_pattern "Slack token" 'xox[abprs]-[A-Za-z0-9-]{10,}'
49+
check_pattern "Google API key" 'AIza[0-9A-Za-z_-]{35}'
50+
check_pattern "Stripe live key" 'sk_live_[A-Za-z0-9]{24,}'
51+
check_pattern "Honcho-style JWT (likely)" 'eyJ[A-Za-z0-9_-]{20,}\.eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}'
52+
check_pattern "RSA/EC/DSA/OpenSSH private key block" '-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----'
53+
check_pattern "Generic hardcoded password" '(password|passwd|pwd)[[:space:]]*[:=][[:space:]]*["'\'']\w{8,}["'\'']'
54+
55+
if [ $FOUND -eq 1 ]; then
56+
printf '\n\033[31m✗ Secret scan: potential secrets in staged changes\033[0m\n' >&2
57+
printf '%b' "$FINDINGS" >&2
58+
printf '\n' >&2
59+
printf 'If this is a false positive, bypass with: \033[33mgit commit --no-verify\033[0m\n' >&2
60+
printf 'Otherwise: remove the secret, rotate the credential, and re-stage.\n\n' >&2
61+
exit 1
62+
fi
63+
64+
exit 0

0 commit comments

Comments
 (0)