#!/usr/bin/env bash set -euo pipefail # Colors RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' echo -e "${GREEN}Running pre-push checks...${NC}" # Paths REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" CONTENT_DIR="$REPO_ROOT/core/content" MKDOCS_CFG_PATH="$CONTENT_DIR/mkdocs.yml" SITE_DIR="$REPO_ROOT/site" LOG_DIR="$REPO_ROOT/test/log" # Clean logs rm -rf "$LOG_DIR"; mkdir -p "$LOG_DIR" SERVER_PID="" cleanup() { if [[ -n "${SERVER_PID:-}" ]] && ps -p "$SERVER_PID" >/dev/null 2>&1; then kill "$SERVER_PID" >/dev/null 2>&1 || true for _ in {1..30}; do ps -p "$SERVER_PID" >/dev/null 2>&1 || break; sleep 0.1; done fi } trap cleanup EXIT INT TERM # --- Optional lightweight checks (keep or remove) --- if command -v trivy &>/dev/null; then echo -e "${GREEN}Running Trivy scan...${NC}" trivy fs . --exit-code 1 --severity CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN --no-progress \ >"$LOG_DIR/trivy.log" 2>&1 || { echo -e "${RED}Trivy failed. See $LOG_DIR/trivy.log${NC}"; exit 1; } else echo -e "${YELLOW}Trivy not installed. Skipping vulnerability scan.${NC}" fi if command -v trufflehog &>/dev/null && command -v jq &>/dev/null; then echo -e "${GREEN}Running TruffleHog (verified only)...${NC}" TMPF="$(mktemp)" trufflehog filesystem . --json >"$TMPF" 2>"$LOG_DIR/trufflehog.log" || true VERIFIED="$(jq 'select(.verified==true)' "$TMPF" | wc -l | tr -d ' ')" if [[ "$VERIFIED" -gt 0 ]]; then cp "$TMPF" "$LOG_DIR/trufflehog-findings.json" echo -e "${RED}Verified secrets found. See $LOG_DIR/trufflehog-findings.json${NC}" rm -f "$TMPF"; exit 1 fi rm -f "$TMPF" else echo -e "${YELLOW}TruffleHog or jq not installed. Skipping secrets scan.${NC}" fi if command -v markdownlint &>/dev/null; then echo -e "${GREEN}Running markdownlint...${NC}" MD_FILES="$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.md$' || true)" if [[ -n "$MD_FILES" ]]; then # shellcheck disable=SC2086 echo $MD_FILES | xargs markdownlint >"$LOG_DIR/markdownlint.log" 2>&1 || { echo -e "${RED}markdownlint failed. See $LOG_DIR/markdownlint.log${NC}"; exit 1; } fi else echo -e "${YELLOW}markdownlint not installed. Skipping markdown check.${NC}" fi if command -v vale &>/dev/null && [[ -f "$REPO_ROOT/.vale.ini" ]]; then echo -e "${GREEN}Running Vale...${NC}" VALE_FILES="$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.md$' || true)" if [[ -n "$VALE_FILES" ]]; then # shellcheck disable=SC2086 echo $VALE_FILES | xargs vale >"$LOG_DIR/vale.log" 2>&1 || { echo -e "${RED}Vale issues. See $LOG_DIR/vale.log${NC}"; exit 1; } fi else echo -e "${YELLOW}Vale not installed or .vale.ini missing. Skipping Vale.${NC}" fi # --- MkDocs build (strict) --- if ! command -v mkdocs >/dev/null 2>&1; then echo -e "${RED}MkDocs not installed; cannot build docs.${NC}"; exit 1 fi echo -e "${GREEN}Building documentation (strict)…${NC}" mkdocs build -f "$MKDOCS_CFG_PATH" -d "$SITE_DIR" --strict >"$LOG_DIR/mkdocs-build.log" 2>&1 || { echo -e "${RED}MkDocs build failed. See $LOG_DIR/mkdocs-build.log${NC}"; exit 1; } # --- Serve built site --- # if ! command -v python3 >/dev/null 2>&1; then # echo -e "${RED}python3 not found; cannot serve static site.${NC}"; exit 1 # fi # PORT="$(python3 - <<'PY' # import socket # s=socket.socket(); s.bind(('127.0.0.1',0)) # print(s.getsockname()[1]); s.close() # PY # )" # STATIC_LOG="$LOG_DIR/static-serve.log" # echo -e "${GREEN}Serving built site at http://127.0.0.1:$PORT …${NC}" # python3 -m http.server "$PORT" --bind 127.0.0.1 --directory "$SITE_DIR" >"$STATIC_LOG" 2>&1 & # SERVER_PID="$!" # Wait for server # echo -e "${GREEN}Waiting for local server to be reachable…${NC}" # START_TIME="$(date +%s)"; TIMEOUT=60 # while : ; do # if ! ps -p "$SERVER_PID" >/dev/null 2>&1; then # echo -e "${RED}Static server died during startup. See $STATIC_LOG${NC}"; exit 1 # fi # if curl --fail --silent -I "http://127.0.0.1:$PORT/" >/dev/null 2>>"$STATIC_LOG"; then break; fi # if (( $(date +%s) - START_TIME >= TIMEOUT )); then # echo -e "${RED}Static server did not come up in ${TIMEOUT}s.${NC}" # tail -n 50 "$STATIC_LOG" || true; exit 1 # fi # sleep 0.5 # done # --- Build URL list (all *.html pages) --- # URLS_FILE="$LOG_DIR/urls.txt" # BASE_URL="http://127.0.0.1:$PORT/" # export SITE_DIR BASE_URL # python3 - <<'PY' > "$URLS_FILE" # import os # site = os.environ['SITE_DIR']; base = os.environ['BASE_URL'] # urls = [] # for root, _, files in os.walk(site): # for name in files: # if name.endswith('.html'): # rel = os.path.relpath(os.path.join(root, name), site).replace(os.sep, '/') # urls.append(base + rel) # for u in sorted(urls): print(u) # PY # TOTAL="$(wc -l < "$URLS_FILE" | tr -d ' ')" # echo -e "${GREEN}Lychee will scan ${TOTAL} pages…${NC}" # # --- Lychee (single run, unbuffered, higher FD limit) --- # if ! command -v lychee >/dev/null 2>&1; then # echo -e "${YELLOW}Lychee not installed. Skipping link check.${NC}" # cleanup; exit 0 # fi # # Raise macOS open-file limit (best-effort) to avoid mid-run aborts # ulimit -n 4096 || true # CSV_OUT="$LOG_DIR/lychee.csv" # RUN_LOG="$LOG_DIR/lychee-run.log" # echo -e "${GREEN}Starting Lychee…${NC}" # set +e # # Use script(1) to allocate a PTY so output is line-buffered; tee to file. # # Capture Lychee's real exit code via PIPESTATUS. # script -q /dev/null lychee \ # --base-url "http://127.0.0.1:$PORT/" \ # --format detailed \ # --output "$CSV_OUT" \ # --verbose \ # --no-progress \ # --max-concurrency 16 \ # --timeout 30 \ # --max-retries 2 \ # --retry-wait-time 2 \ # --accept '100..=103,200..=399' \ # --exclude 'https?://(www\.)?github\.com/spread-ai' \ # --exclude 'https?://[^/]*app\.spread\.ai' \ # --exclude 'https?://[^/]*stage\.spread\.ai' \ # --exclude '\?q=' \ # --exclude '#__.*_annotation_.*' \ # - < "$URLS_FILE" 2>&1 | tee "$RUN_LOG" # LYCHEE_RC=${PIPESTATUS[1]} # set -e # Stop server # cleanup # # Summarize and exit appropriately # if [[ "$LYCHEE_RC" -ne 0 ]]; then # echo -e "${RED}Lychee reported problems (exit $LYCHEE_RC). See:${NC}" # echo " - $RUN_LOG" # echo " - $CSV_OUT" # echo " - $STATIC_LOG" # exit 1 # else # echo -e "${GREEN}Lychee completed successfully. See $CSV_OUT${NC}" # fi