Skip to content
Open
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
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,14 @@ TW_SERVER_PORT=3000
# TW_OTPAAS_NAMESPACE=
# TW_OTPAAS_SECRET=
# TW_OTPAAS_TIMEOUT=10s

# Pre-push hook overrides β€” set before git push, not loaded by the application.
# Each override prints a warning; document the justification in your PR description.
# SKIP_PKG_CHECK=1 # new package reviewed manually (publisher, source repo, download count)
# SKIP_SECRET_SCAN=1 # false positive confirmed
# SKIP_CAST_CHECK=1 # cast is intentional and cannot be avoided
# SKIP_SUPPRESS_CHECK=1 # suppression is justified
# SKIP_TSC=1 # type errors will be fixed in follow-up PR
# SKIP_GO_TESTS=1 # test failures will be fixed in follow-up PR
# SKIP_GO_LINT=1 # lint issues will be fixed in follow-up PR
# SKIP_FRONTEND_TESTS=1 # test failures will be fixed in follow-up PR
186 changes: 186 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env bash
set -euo pipefail

RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BOLD='\033[1m'
NC='\033[0m'

FAILURES=()
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
fail() { echo -e "${RED}[fail]${NC} $*"; FAILURES+=("$*"); }
pass() { echo -e "${GREEN}[pass]${NC} $*"; }
check() { echo -e "\n${BOLD}>>>${NC} $*"; }

# ── Changed file sets ──────────────────────────────────────────────────────────
BASE=$(git merge-base HEAD origin/main 2>/dev/null \
|| git merge-base HEAD main 2>/dev/null \
|| git rev-parse HEAD~1)
CHANGED=$(git diff --name-only "$BASE"...HEAD)

# TS source files (not tests) β€” for cast / suppression checks
CHANGED_TS=$(printf '%s\n' "$CHANGED" | grep -E '\.(ts|tsx)$' || true)
CHANGED_TS_SRC=$(printf '%s\n' "$CHANGED_TS" | grep -Ev '\.(test|spec)\.(ts|tsx)$' || true)

# Go source files (not tests) β€” for lint / test checks
CHANGED_GO_SRC=$(printf '%s\n' "$CHANGED" | grep '\.go$' | grep -v '_test\.go$' || true)

# ── 1. PACKAGE SAFETY ─────────────────────────────────────────────────────────
# Runs first: if a new package is suspect we don't want to execute its code in tests.
PKG_CHANGED=$(printf '%s\n' "$CHANGED" | grep '^package\.json$' || true)
if [[ -n "$PKG_CHANGED" ]]; then
check "Package safety"

NEW_PKGS=$(git diff "$BASE"...HEAD -- package.json \
| grep '^+' | grep -v '^+++' \
| grep -E '"[^"]+":\s*"[^"]+"' \
| grep -v '"version"\|"name"\|"description"\|"license"' \
|| true)

if [[ "${SKIP_PKG_CHECK:-0}" == "1" ]]; then
warn "Package check skipped (SKIP_PKG_CHECK=1) β€” confirm in PR description:"
warn " - Publisher identity verified"
warn " - Source repository reviewed"
warn " - Download count and last publish date checked"
[[ -n "$NEW_PKGS" ]] && echo "$NEW_PKGS"
elif [[ -n "$NEW_PKGS" ]]; then
echo ""
echo -e "${YELLOW}New or changed packages in package.json:${NC}"
echo "$NEW_PKGS"
echo ""
echo "Running pnpm audit..."
if ! pnpm audit 2>&1; then
fail "pnpm audit found vulnerabilities β€” resolve them before pushing (override: SKIP_PKG_CHECK=1 git push)"
else
fail "New packages require manual review. Verify each, then re-push with SKIP_PKG_CHECK=1 and document in PR:
- Publisher identity (known org or trusted individual?)
- Source repository (real code or thin wrapper?)
- Download count and age (established or brand-new?)
- Last publish date (recent unusual publish is a supply-chain red flag)"
fi
else
pass "No new packages detected"
fi
fi

# ── 2. SECRET SCANNING ────────────────────────────────────────────────────────
if [[ "${SKIP_SECRET_SCAN:-0}" != "1" ]]; then
check "Secret scanning"
if make secrets BASE="$BASE" 2>&1; then
pass "No secrets detected"
else
fail "Potential secrets detected by gitleaks (override: SKIP_SECRET_SCAN=1 git push β€” document false-positive in PR)"
fi
else
warn "Secret scan skipped (SKIP_SECRET_SCAN=1) β€” document justification in PR description"
fi

# ── 3. UNSAFE TYPE CASTS ──────────────────────────────────────────────────────
# Source files only β€” test stubs sometimes legitimately need loose types.
if [[ -n "$CHANGED_TS_SRC" ]] && [[ "${SKIP_CAST_CHECK:-0}" != "1" ]]; then
check "Unsafe TypeScript casts"
DOUBLE_CASTS=$(printf '%s\n' "$CHANGED_TS_SRC" | xargs grep -n 'as unknown as' 2>/dev/null || true)
ANY_CASTS=$(printf '%s\n' "$CHANGED_TS_SRC" | xargs grep -n '\bas any\b' 2>/dev/null || true)
if [[ -n "$DOUBLE_CASTS" ]] || [[ -n "$ANY_CASTS" ]]; then
fail "Unsafe type casts found β€” resolve the underlying type error (override: SKIP_CAST_CHECK=1 git push)"
[[ -n "$DOUBLE_CASTS" ]] && echo " as unknown as (double cast):" && echo "$DOUBLE_CASTS"
[[ -n "$ANY_CASTS" ]] && echo " as any:" && echo "$ANY_CASTS"
else
pass "No unsafe type casts"
fi
elif [[ "${SKIP_CAST_CHECK:-0}" == "1" ]]; then
warn "Cast check skipped (SKIP_CAST_CHECK=1) β€” document justification in PR description"
fi

# ── 4. LINT / TYPE SUPPRESSION COMMENTS ──────────────────────────────────────
# @ts-expect-error is allowed in test files (legitimate for asserting type errors).
# All other suppressions are flagged in all TS files.
if [[ -n "$CHANGED_TS" ]] && [[ "${SKIP_SUPPRESS_CHECK:-0}" != "1" ]]; then
check "Lint and type suppression comments"

ALL_SUPPRESSED=$(printf '%s\n' "$CHANGED_TS" | xargs grep -n \
-e 'eslint-disable' \
-e 'oxlint-disable' \
-e '@ts-ignore' \
-e '@ts-nocheck' \
2>/dev/null || true)

# @ts-expect-error only flagged in source, not test files
EXPECT_ERR=$(printf '%s\n' "$CHANGED_TS_SRC" | xargs grep -n '@ts-expect-error' 2>/dev/null || true)

if [[ -n "$ALL_SUPPRESSED" ]] || [[ -n "$EXPECT_ERR" ]]; then
fail "Lint/type suppression comments found β€” fix the underlying issue (override: SKIP_SUPPRESS_CHECK=1 git push)"
[[ -n "$ALL_SUPPRESSED" ]] && echo "$ALL_SUPPRESSED"
[[ -n "$EXPECT_ERR" ]] && echo " @ts-expect-error in source files (allowed in *.test.ts only):" && echo "$EXPECT_ERR"
else
pass "No suppression comments"
fi
elif [[ "${SKIP_SUPPRESS_CHECK:-0}" == "1" ]]; then
warn "Suppression check skipped (SKIP_SUPPRESS_CHECK=1) β€” document justification in PR description"
fi

# ── 5. TYPESCRIPT TYPE CHECK ─────────────────────────────────────────────────
if [[ -n "$CHANGED_TS" ]] && [[ "${SKIP_TSC:-0}" != "1" ]]; then
check "TypeScript type check"
if pnpm exec tsc --noEmit 2>&1; then
pass "TypeScript types OK"
else
fail "TypeScript type errors β€” fix errors rather than suppressing them (override: SKIP_TSC=1 git push)"
fi
elif [[ "${SKIP_TSC:-0}" == "1" ]]; then
warn "TypeScript check skipped (SKIP_TSC=1)"
fi

# ── 6. GO TESTS ───────────────────────────────────────────────────────────────
if [[ -n "$CHANGED_GO_SRC" ]] && [[ "${SKIP_GO_TESTS:-0}" != "1" ]]; then
check "Go tests"
if go test ./...; then
pass "Go tests passed"
else
fail "Go tests failed (override: SKIP_GO_TESTS=1 git push)"
fi
elif [[ "${SKIP_GO_TESTS:-0}" == "1" ]]; then
warn "Go tests skipped (SKIP_GO_TESTS=1)"
fi

# ── 7. GO LINT ────────────────────────────────────────────────────────────────
if [[ -n "$CHANGED_GO_SRC" ]] && [[ "${SKIP_GO_LINT:-0}" != "1" ]]; then
check "Go lint (golangci-lint)"
if make lint; then
pass "Go lint passed"
else
fail "Go lint failed (override: SKIP_GO_LINT=1 git push)"
fi
elif [[ "${SKIP_GO_LINT:-0}" == "1" ]]; then
warn "Go lint skipped (SKIP_GO_LINT=1)"
fi

# ── 8. FRONTEND TESTS ─────────────────────────────────────────────────────────
if [[ -f vitest.config.ts ]] && [[ -n "$CHANGED_TS" ]] && [[ "${SKIP_FRONTEND_TESTS:-0}" != "1" ]]; then
check "Frontend tests"
if pnpm test; then
pass "Frontend tests passed"
else
fail "Frontend tests failed (override: SKIP_FRONTEND_TESTS=1 git push)"
fi
elif [[ "${SKIP_FRONTEND_TESTS:-0}" == "1" ]]; then
warn "Frontend tests skipped (SKIP_FRONTEND_TESTS=1)"
fi

# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
if [[ ${#FAILURES[@]} -gt 0 ]]; then
echo -e "${RED}${BOLD}Pre-push blocked β€” ${#FAILURES[@]} check(s) failed:${NC}"
for f in "${FAILURES[@]}"; do
echo -e " ${RED}β€’${NC} $f"
done
echo ""
echo "Per-check overrides (set env var before git push β€” document all overrides in PR):"
echo " SKIP_PKG_CHECK=1 / SKIP_SECRET_SCAN=1 / SKIP_CAST_CHECK=1 / SKIP_SUPPRESS_CHECK=1"
echo " SKIP_TSC=1 / SKIP_GO_TESTS=1 / SKIP_GO_LINT=1 / SKIP_FRONTEND_TESTS=1"
echo "Nuclear override (emergencies only): git push --no-verify"
exit 1
fi

echo -e "${GREEN}${BOLD}All pre-push checks passed.${NC}"
1 change: 1 addition & 0 deletions .lintstagedrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
export default {
'*.{js,jsx,ts,tsx}': 'oxlint --fix',
'*.{js,jsx,ts,tsx,md,html,css,json,jsonc,yaml,toml}': 'oxfmt --write',
'*.go': () => 'make lint',
};
52 changes: 52 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,58 @@ Use the format `<type>/<short-description>`, where `<type>` mirrors the [Commit

Examples: `feat/session-middleware`, `fix/server-startup-race`, `docs/contributing-guide`.

## Git Hooks

Two Husky hooks run automatically. Both require `pnpm install` and `make tools` to be run first (see [First-time setup](#first-time-setup)).

### Pre-commit

Runs on staged files only. Fast β€” does not run tests.

| What runs | Scope |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| `oxlint --fix` | Staged `.ts`, `.tsx`, `.js`, `.jsx` files |
| `oxfmt --write` | Staged `.ts`, `.tsx`, `.js`, `.jsx`, `.md`, `.json`, `.yaml`, and other format-supported files |
| `golangci-lint run` | Staged `.go` files (requires `make tools`) |

### Pre-push

Runs against all files changed on your branch vs `main`. Slower β€” runs the full test suite and additional AI-agent mispractice guards.

| # | Check | Trigger |
| --- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------- |
| 1 | Package safety: `pnpm audit` + manual-review gate | `package.json` changed |
| 2 | Secret scanning (`gitleaks`) | Always |
| 3 | Unsafe TypeScript casts (`as any`, `as unknown as X`) | TS source files changed |
| 4 | Lint/type suppression comments (`eslint-disable`, `oxlint-disable`, `@ts-ignore`, `@ts-nocheck`) | TS files changed |
| 5 | TypeScript type check (`tsc --noEmit`) | TS files changed |
| 6 | Go tests (`go test ./...`) | Go source files changed |
| 7 | Go lint (`golangci-lint run`) | Go source files changed |
| 8 | Frontend tests (`pnpm test`) | TS files changed and `vitest.config.ts` present |

Note: `@ts-expect-error` is allowed in `*.test.ts` / `*.test.tsx` files (legitimate for asserting type errors) but blocked in source files.

### Overriding a check

Set the relevant env var before `git push`. Every override prints a warning β€” document the justification in your PR description.

```bash
SKIP_PKG_CHECK=1 git push # new package reviewed manually
SKIP_SECRET_SCAN=1 git push # false positive confirmed
SKIP_CAST_CHECK=1 git push # cast is intentional
SKIP_SUPPRESS_CHECK=1 git push # suppression is justified
SKIP_TSC=1 git push # type errors addressed in follow-up
SKIP_GO_TESTS=1 git push # tests addressed in follow-up
SKIP_GO_LINT=1 git push # lint issues addressed in follow-up
SKIP_FRONTEND_TESTS=1 git push # tests addressed in follow-up
```

To skip all hooks (emergencies only):

```bash
git push --no-verify
```

## Code Style

Don't use em-dashes (`β€”`) in code, comments, or documentation. Use colons, parentheses, or separate sentences instead.
Expand Down
18 changes: 15 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
SHELL := /bin/bash
BIN := $(CURDIR)/bin
EXE := $(if $(filter Windows_NT,$(OS)),.exe,)

GOLANGCI_VERSION := v2.11.4
GOLANGCI_LINT := $(BIN)/golangci-lint-$(GOLANGCI_VERSION)
GOLANGCI_LINT := $(BIN)/golangci-lint-$(GOLANGCI_VERSION)$(EXE)

GITLEAKS_VERSION := v8.27.2
GITLEAKS := $(BIN)/gitleaks-$(GITLEAKS_VERSION)$(EXE)

$(BIN):
mkdir -p $(BIN)

$(GOLANGCI_LINT): | $(BIN)
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(BIN) $(GOLANGCI_VERSION)
mv $(BIN)/golangci-lint $@
mv $(BIN)/golangci-lint$(EXE) $@

$(GITLEAKS): | $(BIN)
GOBIN=$(BIN) go install github.com/zricethezav/gitleaks/v8@$(GITLEAKS_VERSION)
mv $(BIN)/gitleaks$(EXE) $@

.PHONY: tools
tools: $(GOLANGCI_LINT)
tools: $(GOLANGCI_LINT) $(GITLEAKS)

.PHONY: lint
lint: $(GOLANGCI_LINT)
$(GOLANGCI_LINT) run

.PHONY: secrets
secrets: $(GITLEAKS)
$(GITLEAKS) git --log-opts="$(BASE)..HEAD"
Loading
Loading