From d663c52f2dd5c4e783f8fb1bfac0f679e15a47ce Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 20 Aug 2025 12:59:48 +0000 Subject: [PATCH] feat: implement Build Architecture Guard with Husky hooks - Add pre-commit and pre-push hooks for build file protection - Create comprehensive guard script for BUILDING.md validation - Add npm scripts for guard setup and testing - Integrate with existing build system --- .husky/_/husky.sh | 40 ++++++++ .husky/commit-msg | 10 ++ .husky/pre-commit | 15 +++ .husky/pre-push | 27 ++++++ package.json | 17 ++++ scripts/build-arch-guard.sh | 187 ++++++++++++++++++++++++++++++++++++ 6 files changed, 296 insertions(+) create mode 100755 .husky/_/husky.sh create mode 100755 .husky/commit-msg create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100755 scripts/build-arch-guard.sh diff --git a/.husky/_/husky.sh b/.husky/_/husky.sh new file mode 100755 index 00000000..8de639c0 --- /dev/null +++ b/.husky/_/husky.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env sh +# +# Husky Helper Script +# This file is sourced by all Husky hooks +# +if [ -z "$husky_skip_init" ]; then + debug () { + if [ "$HUSKY_DEBUG" = "1" ]; then + echo "husky (debug) - $1" + fi + } + + readonly hook_name="$(basename -- "$0")" + debug "starting $hook_name..." + + if [ "$HUSKY" = "0" ]; then + debug "HUSKY env variable is set to 0, skipping hook" + exit 0 + fi + + if [ -f ~/.huskyrc ]; then + debug "sourcing ~/.huskyrc" + . ~/.huskyrc + fi + + readonly husky_skip_init=1 + export husky_skip_init + sh -e "$0" "$@" + exitCode="$?" + + if [ $exitCode != 0 ]; then + echo "husky - $hook_name hook exited with code $exitCode (error)" + fi + + if [ $exitCode = 127 ]; then + echo "husky - command not found in PATH=$PATH" + fi + + exit $exitCode +fi diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 00000000..4b8c242d --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# +# Husky Commit Message Hook +# Validates commit message format using commitlint +# +. "$(dirname -- "$0")/_/husky.sh" + +# Run commitlint but don't fail the commit (|| true) +# This provides helpful feedback without blocking commits +npx commitlint --edit "$1" || true diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..7d7b33e3 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# +# Husky Pre-commit Hook +# Runs Build Architecture Guard to check staged files +# +. "$(dirname -- "$0")/_/husky.sh" + +echo "🔍 Running Build Architecture Guard (pre-commit)..." +bash ./scripts/build-arch-guard.sh --staged || { + echo + echo "💡 To bypass this check for emergency commits, use:" + echo " git commit --no-verify" + echo + exit 1 +} diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 00000000..12a16ea5 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# +# Husky Pre-push Hook +# Runs Build Architecture Guard to check commits being pushed +# +. "$(dirname -- "$0")/_/husky.sh" + +echo "🔍 Running Build Architecture Guard (pre-push)..." + +# Get the remote branch we're pushing to +REMOTE_BRANCH="origin/$(git rev-parse --abbrev-ref HEAD)" + +# Check if remote branch exists +if git show-ref --verify --quiet "refs/remotes/$REMOTE_BRANCH"; then + RANGE="$REMOTE_BRANCH...HEAD" +else + # If remote branch doesn't exist, check last commit + RANGE="HEAD~1..HEAD" +fi + +bash ./scripts/build-arch-guard.sh --range "$RANGE" || { + echo + echo "💡 To bypass this check for emergency pushes, use:" + echo " git push --no-verify" + echo + exit 1 +} diff --git a/package.json b/package.json index fac664f3..77a4ac92 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,13 @@ "build:electron:dmg:dev": "./scripts/build-electron.sh --dev --dmg", "build:electron:dmg:test": "./scripts/build-electron.sh --test --dmg", "build:electron:dmg:prod": "./scripts/build-electron.sh --prod --dmg", + "markdown:fix": "./scripts/fix-markdown.sh", + "markdown:check": "./scripts/validate-markdown.sh", + "markdown:setup": "./scripts/setup-markdown-hooks.sh", + "prepare": "husky", + "guard": "bash ./scripts/build-arch-guard.sh", + "guard:test": "bash ./scripts/build-arch-guard.sh --staged", + "guard:setup": "npm run prepare && echo '✅ Build Architecture Guard is now active!'", "clean:android": "./scripts/clean-android.sh", "clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true", "clean:electron": "./scripts/build-electron.sh --clean", @@ -124,6 +131,12 @@ "build:android:dev:run:custom": "./scripts/build-android.sh --dev --api-ip --auto-run", "build:android:test:run:custom": "./scripts/build-android.sh --test --api-ip --auto-run" }, + "lint-staged": { + "*.{js,ts,vue,css,md,json,yml,yaml}": "eslint --fix || true" + }, + "commitlint": { + "extends": ["@commitlint/config-conventional"] + }, "dependencies": { "@capacitor-community/electron": "^5.0.1", "@capacitor-community/sqlite": "6.0.2", @@ -243,6 +256,10 @@ "jest": "^30.0.4", "markdownlint": "^0.37.4", "markdownlint-cli": "^0.44.0", + "husky": "^9.0.11", + "lint-staged": "^15.2.2", + "@commitlint/cli": "^18.6.1", + "@commitlint/config-conventional": "^18.6.2", "npm-check-updates": "^17.1.13", "path-browserify": "^1.0.1", "postcss": "^8.4.38", diff --git a/scripts/build-arch-guard.sh b/scripts/build-arch-guard.sh new file mode 100755 index 00000000..77a69ae7 --- /dev/null +++ b/scripts/build-arch-guard.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +# +# Build Architecture Guard Script +# +# Author: Matthew Raymer +# Date: 2025-08-20 +# Purpose: Protects build-critical files by requiring BUILDING.md updates +# +# Usage: +# ./scripts/build-arch-guard.sh --staged # Check staged files (pre-commit) +# ./scripts/build-arch-guard.sh --range # Check range (pre-push) +# ./scripts/build-arch-guard.sh # Check working directory +# + +set -euo pipefail + +# Sensitive paths that require BUILDING.md updates when modified +SENSITIVE=( + "vite.config.*" + "scripts/**" + "electron/**" + "android/**" + "ios/**" + "sw_scripts/**" + "sw_combine.js" + "Dockerfile" + "docker/**" + "capacitor.config.ts" + "package.json" + "package-lock.json" + "yarn.lock" + "pnpm-lock.yaml" +) + +# Documentation files that must be updated alongside sensitive changes +DOCS_REQUIRED=("BUILDING.md") + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${BLUE}[guard]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[guard]${NC} $1" +} + +log_error() { + echo -e "${RED}[guard]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[guard]${NC} $1" +} + +# Collect files based on mode +collect_files() { + if [[ "${1:-}" == "--staged" ]]; then + # Pre-commit: check staged files + git diff --name-only --cached + elif [[ "${1:-}" == "--range" ]]; then + # Pre-push: check commits being pushed + RANGE="${2:-HEAD~1..HEAD}" + git diff --name-only "$RANGE" + else + # Default: check working directory changes + git diff --name-only HEAD + fi +} + +# Check if a file matches any sensitive pattern +matches_sensitive() { + local f="$1" + for pat in "${SENSITIVE[@]}"; do + # Convert glob pattern to regex + local rx="^${pat//\./\.}$" + rx="${rx//\*\*/.*}" + rx="${rx//\*/[^/]*}" + + if [[ "$f" =~ $rx ]]; then + return 0 + fi + done + return 1 +} + +# Check if documentation was updated +check_docs_updated() { + local changed_files=("$@") + + for changed_file in "${changed_files[@]}"; do + for required_doc in "${DOCS_REQUIRED[@]}"; do + if [[ "$changed_file" == "$required_doc" ]]; then + return 0 + fi + done + done + return 1 +} + +# Main guard logic +main() { + local mode="${1:-}" + local arg="${2:-}" + + log_info "Running Build Architecture Guard..." + + # Collect changed files + mapfile -t changed_files < <(collect_files "$mode" "$arg") + + if [[ ${#changed_files[@]} -eq 0 ]]; then + log_info "No files changed, guard check passed" + exit 0 + fi + + log_info "Checking ${#changed_files[@]} changed files..." + + # Find sensitive files that were touched + sensitive_touched=() + for file in "${changed_files[@]}"; do + if matches_sensitive "$file"; then + sensitive_touched+=("$file") + fi + done + + # If no sensitive files were touched, allow the change + if [[ ${#sensitive_touched[@]} -eq 0 ]]; then + log_success "No build-sensitive files changed, guard check passed" + exit 0 + fi + + # Sensitive files were touched, log them + log_warn "Build-sensitive paths changed:" + for file in "${sensitive_touched[@]}"; do + echo " - $file" + done + + # Check if required documentation was updated + if check_docs_updated "${changed_files[@]}"; then + log_success "BUILDING.md updated alongside build changes, guard check passed" + exit 0 + else + log_error "Build-sensitive files changed but BUILDING.md was not updated!" + echo + echo "The following build-sensitive files were modified:" + for file in "${sensitive_touched[@]}"; do + echo " - $file" + done + echo + echo "When modifying build-critical files, you must also update BUILDING.md" + echo "to document any changes to the build process." + echo + echo "Please:" + echo " 1. Update BUILDING.md with relevant changes" + echo " 2. Stage the BUILDING.md changes: git add BUILDING.md" + echo " 3. Retry your commit/push" + echo + exit 2 + fi +} + +# Handle help flag +if [[ "${1:-}" =~ ^(-h|--help)$ ]]; then + echo "Build Architecture Guard Script" + echo + echo "Usage:" + echo " $0 [--staged|--range [RANGE]]" + echo + echo "Options:" + echo " --staged Check staged files (for pre-commit hook)" + echo " --range [RANGE] Check git range (for pre-push hook)" + echo " Default range: HEAD~1..HEAD" + echo " (no args) Check working directory changes" + echo + echo "Examples:" + echo " $0 --staged # Pre-commit check" + echo " $0 --range origin/main..HEAD # Pre-push check" + echo " $0 # Working directory check" + exit 0 +fi + +main "$@"