From 66d8f14efe8570ad345d61ea3b6b8f59ca72d667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Fri, 12 Dec 2025 10:03:35 +0700 Subject: [PATCH 1/6] improved runtime of pre-commit significantly --- .husky/pre-commit | 581 ++++++++++++++++++++++++++++------------------ 1 file changed, 350 insertions(+), 231 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index f59a5b01a..b06e1d277 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,273 +1,392 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -echo "" -echo "Running 'forge build' and 'typechain generation'..." - -# Run forge build first (required for typechain) -echo "Building contracts with forge..." -forge build --skip test - -if [ $? -ne 0 ]; then - printf '\n%s\n\n' "Forge build failed. Aborting commit." - exit 1 -fi - -echo "Forge build completed successfully!" -echo "" - -# Run typechain generation (now that artifacts are available) -echo "Generating TypeChain types..." -bun typechain:incremental - -if [ $? -ne 0 ]; then - printf '\n%s\n\n' "TypeChain generation failed. Aborting commit." - exit 1 -fi - -echo "TypeChain generation completed successfully!" -echo "" - -# Run TypeScript compilation to catch any type errors from outdated typings -echo "Running TypeScript compilation to check for type errors..." -bunx tsc-files --noEmit - -if [ $? -ne 0 ]; then - printf '\n%s\n' "TypeScript compilation failed. This may indicate outdated TypeChain types." - printf '%s\n\n' "Please run 'bun typechain' manually and fix any type errors before committing." - exit 1 +# Early exit: Check if there are any staged files +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM) +if [ -z "$STAGED_FILES" ]; then + echo "No files staged for commit. Skipping pre-commit checks." + exit 0 fi -echo "" -echo "TypeScript compilation passed - all types are up to date!" -echo "" - -echo "" -echo "Running 'bun lint-staged' now:" -bun lint-staged - -echo "" -echo "" -echo "now checking for .env secrets and private keys accidentally being committed to Github" -echo " > any 64-byte hex string will be identified as potential private key" -echo " > disable false positive matches by commenting '[pre-commit-checker: not a secret]' in same line or line above" -echo " > execution might take a while depending on the size of your git diff " -echo " > logs will only be shown once task is completed" -echo "" - -DISABLE_WITH_COMMENT="pre-commit-checker: not a secret" -# Regex pattern to identify potential Ethereum private keys (64 hexadecimal characters) -ETH_PRIVATE_KEY_PATTERN="\b[a-fA-F0-9]{64}\b" - -# List of known false positive values that should not be flagged as secrets -KNOWN_FALSE_POSITIVES=( - "true" - "false" - "none" - "" - "verifyContract" -) - -# List of paths to exclude from secret checks -EXCLUDED_PATHS=( - "deployments/_deployments_log_file.json" - "config/networks.json" - "lib/" - "safe/cancun/out/" - "safe/london/out/" - "bun.lock" - ".bun/" -) - -# Load secrets from .env file -if [ -f ".env" ]; then - ENV_SECRETS=$(grep -v '^#' .env | sed 's/#.*//' | grep -v '^\s*$' | sed 's/ *$//') -else - echo ".env file not found" - ENV_SECRETS="" -fi +# Determine what types of files changed for conditional execution (optimized single pass) +HAS_SOL_FILES="no" +HAS_TS_JS_FILES="no" +HAS_TYPE_FILES="no" + +echo "$STAGED_FILES" | grep -qE '\.sol$' && HAS_SOL_FILES="yes" && HAS_TYPE_FILES="yes" +echo "$STAGED_FILES" | grep -qE '\.(ts|js|tsx)$' && HAS_TS_JS_FILES="yes" && HAS_TYPE_FILES="yes" + +# Create temp directory for parallel execution +TEMP_DIR=$(mktemp -d) +trap "rm -rf '$TEMP_DIR' 2>/dev/null" EXIT INT TERM + +FORGE_OUTPUT="$TEMP_DIR/forge.out" +FORGE_EXIT="$TEMP_DIR/forge.exit" +TYPECHAIN_OUTPUT="$TEMP_DIR/typechain.out" +TYPECHAIN_EXIT="$TEMP_DIR/typechain.exit" +TSC_OUTPUT="$TEMP_DIR/tsc.out" +TSC_EXIT="$TEMP_DIR/tsc.exit" +LINT_STAGED_OUTPUT="$TEMP_DIR/lint-staged.out" +LINT_STAGED_EXIT="$TEMP_DIR/lint-staged.exit" +SECRET_CHECK_OUTPUT="$TEMP_DIR/secret-check.out" +SECRET_CHECK_EXIT="$TEMP_DIR/secret-check.exit" + +# Helper function to print section header +print_section() { + printf '\n\033[1m━━━ %s ━━━\033[0m\n' "$1" +} -printRed() { - local MESSAGE="$1" - printf '\033[31m%s\033[0m\n' "$MESSAGE" +# Helper function to print status +print_status() { + local status=$1 + local message=$2 + if [ "$status" = "success" ]; then + printf '\033[32m✓\033[0m %s\n' "$message" + elif [ "$status" = "skip" ]; then + printf '\033[33m⊘\033[0m %s\n' "$message" + elif [ "$status" = "error" ]; then + printf '\033[31m✗\033[0m %s\n' "$message" + else + printf ' %s\n' "$message" + fi } -printYellow() { - local MESSAGE="$1" - printf '\033[33m%s\033[0m\n' "$MESSAGE" + +# Helper function to filter verbose output +filter_output() { + local tool=$1 + local content=$2 + + case "$tool" in + forge) + # Only show errors, suppress compilation details + echo "$content" | grep -E "(Error|Failed|error|failed)" || true + ;; + typechain) + # Show only errors and summary, suppress verbose output + echo "$content" | grep -vE "^\$ |^Resolving|^Resolved|^Saved|^Cleaned|^Cleaned duplicate" | grep -E "(Successfully|Error|error|Failed|failed)" || true + ;; + lint-staged) + # Suppress verbose lint-staged output, keep only errors and important messages + echo "$content" | grep -vE "^\[(STARTED|COMPLETED)\]" | grep -vE "^Preparing lint-staged" | grep -vE "^Running tasks" | grep -vE "^Applying modifications" | grep -vE "^Cleaning up" | grep -E "(error|Error|failed|Failed|warning|Warning)" || true + ;; + tsc) + # Show only errors + echo "$content" | grep -vE "^$" | head -20 || true + ;; + *) + echo "$content" + ;; + esac } -printAdvise() { - local ACTION="$1" +# Helper function to wait for PIDs and exit on failure +wait_and_check() { + local pid=$1 + local exit_file=$2 + local output_file=$3 + local error_msg=$4 + local tool_name=$5 - echo "" - printf '\033[91m%s\033[0m\n' "NEXT STEPS" - if [ "$ACTION" = "abort" ]; then - printf '\033[91m%s\033[0m\n' "Remove the secrets and try to commit again" - else - printf '\033[91m%s\033[0m\n' "Check each match carefully and make sure that no sensitive information is being committed" - printf '\033[91m%s\033[0m\n' "If it did happen, undo the commit with 'git reset --soft HEAD~1', remove the secret(s) and commit again." - printf '\033[91m%s\033[0m\n' "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! BEFORE PUSHING TO GITHUB !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + if [ -z "$pid" ]; then + return 0 fi -} -# checks if the given line number or the line above contain a comment to disable the check -isKnownFalsePositiveMatch() { - local FILE="$1" - local LINE_NUMBER="$2" - - if [ "$LINE_NUMBER" -gt 1 ]; then - local PREV_LINE=$(sed "$((LINE_NUMBER - 1))q;d" "$FILE") - local CURR_LINE=$(sed "${LINE_NUMBER}q;d" "$FILE") - - # check if comment is present in line above - if echo "$PREV_LINE" | grep -q "$DISABLE_WITH_COMMENT"; then - return 0 - # check if comment is present in current line - elif echo "$CURR_LINE" | grep -q "$DISABLE_WITH_COMMENT"; then - return 0 - fi - # special handlng if line number is first line (then only check this line) - elif [ "$LINE_NUMBER" -eq 1 ]; then - local CURR_LINE=$(sed "${LINE_NUMBER}q;d" "$FILE") + wait "$pid" 2>/dev/null + local exit_code=$(cat "$exit_file" 2>/dev/null || echo "1") - if echo "$CURR_LINE" | grep -q "$DISABLE_WITH_COMMENT"; then - return 0 + if [ "$exit_code" -ne 0 ] && [ -f "$output_file" ]; then + # Show filtered output only on errors + if [ -n "$tool_name" ]; then + filter_output "$tool_name" "$(cat "$output_file")" + else + cat "$output_file" fi fi - return 1 + if [ "$exit_code" -ne 0 ]; then + [ -n "$error_msg" ] && printf '\n\033[31m%s\033[0m\n\n' "$error_msg" + return 1 + fi + return 0 } -# checks if a file contains any of the secrets in .env -doesFileContainDotEnvSecret() { - local FILE="$1" - # iterate through all secrets - for SECRET in $ENV_SECRETS; do - # extract key and value - local VALUE=$(echo "$SECRET" | cut -d '=' -f 2- | sed -e 's/^["'\''"]*//' -e 's/["'\''"]*$//') - local KEY=$(echo "$SECRET" | cut -d '=' -f 1) - - # skip empty values and known false positives - local IS_FALSE_POSITIVE=false - for FALSE_POSITIVE in "${KNOWN_FALSE_POSITIVES[@]}"; do - if [ "$VALUE" = "$FALSE_POSITIVE" ]; then - IS_FALSE_POSITIVE=true - break - fi - done - - if [ -z "$VALUE" ] || [ "$IS_FALSE_POSITIVE" = true ]; then - continue - fi +# Start secret checking immediately (no dependencies, runs on staged file content to avoid race conditions) +( + # Suppress echo in background process - we'll show it when we wait + : - # go through file line by line - grep -nH "$VALUE" "$FILE" | while IFS= read -r LINE; do - LINE_CONTENT=$(echo "$LINE" | cut -d: -f2-) - - # check if the FILE contains the SECRET value - if [ "$LINE_CONTENT" != "" ] && echo "$LINE_CONTENT" | grep -q "$VALUE"; then - # match found - LINE_NUMBER=$(echo "$LINE" | cut -d: -f2) - # check if this is a known false positive marked by a comment in the code - if ! isKnownFalsePositiveMatch "$FILE" "$LINE_NUMBER"; then - # print match - echo "[$FILE:$LINE_NUMBER] Secret from .env file found (key: $KEY)" - fi - fi - done - done -} + DISABLE_WITH_COMMENT="pre-commit-checker: not a secret" + ETH_PRIVATE_KEY_PATTERN="[a-fA-F0-9]{64}" + EXCLUDED_PATHS="deployments/_deployments_log_file.json config/networks.json lib/ safe/cancun/out/ safe/london/out/ bun.lock .bun/" -# checks if a file contains a potential private key (=> a 64 hex string) -doesFileContainPotentialPrivateKey() { - local FILE="$1" + # Parse .env file once (optimized) + ENV_SECRETS="" + if [ -f ".env" ]; then + ENV_SECRETS=$(awk -F= '/^[^#]/ && NF >= 2 { + gsub(/^[ \t]*["'\''"]*|["'\''"]*[ \t]*$/, "", $2) + if ($2 != "" && $2 != "true" && $2 != "false" && $2 != "none" && $2 != "verifyContract") { + print $1 "=" $2 + } + }' .env) + fi - # check file content for matches with private key regEx - local MATCHES=$(grep -E -nH "$ETH_PRIVATE_KEY_PATTERN" "$FILE") + # Optimized secret checking script - single pass for all checks + TEMP_SCRIPT=$(mktemp) + cat > "$TEMP_SCRIPT" << EOFFUNC +#!/bin/sh +FILE="\$1" +DISABLE_COMMENT="$DISABLE_WITH_COMMENT" +PRIVKEY_PATTERN="$ETH_PRIVATE_KEY_PATTERN" +ENV_SECRETS="$ENV_SECRETS" +EXCLUDED_PATHS="$EXCLUDED_PATHS" + +# Fast exclusion check +for EXCLUDED in \$EXCLUDED_PATHS; do + case "\$FILE" in + \$EXCLUDED*) exit 0 ;; + esac +done + +# Get staged file content (avoids race conditions with lint-staged modifications) +FILE_CONTENT=\$(git show ":\$FILE" 2>/dev/null) +[ -z "\$FILE_CONTENT" ] && exit 0 + +# Combined single-pass check for both private keys and secrets (maximum efficiency) +echo "\$FILE_CONTENT" | awk -v file="\$FILE" -v comment="\$DISABLE_COMMENT" -v env_secrets="\$ENV_SECRETS" ' +BEGIN { + found_privkey = 0 + prev_line = "" + + # Build secret lookup array + if (env_secrets != "") { + n = split(env_secrets, secrets, "\n") + for (i = 1; i <= n; i++) { + if (secrets[i] != "") { + eq_pos = index(secrets[i], "=") + if (eq_pos > 0) { + key = substr(secrets[i], 1, eq_pos - 1) + value = substr(secrets[i], eq_pos + 1) + gsub(/^[ \t]*["'\''"]+|["'\''"]*[ \t]*$/, "", value) + if (value != "" && length(value) > 3) { + secret_values[++num_secrets] = value + secret_keys[value] = key + } + } + } + } + } +} +{ + line_num = NR + curr_line = \$0 + + # Check for private keys (64 hex chars as standalone token) + if (match(curr_line, /(^|[^0-9a-fA-F])[0-9a-fA-F]{64}([^0-9a-fA-F]|$)/)) { + match_pos = RSTART + if (substr(curr_line, match_pos, 1) !~ /[0-9a-fA-F]/) { + match_pos = match_pos + 1 + } + # Verify it's exactly 64, not part of longer hex + if (substr(curr_line, match_pos + 64, 1) !~ /[0-9a-fA-F]/) { + disabled = (prev_line ~ comment || curr_line ~ comment) + if (!disabled) { + if (!found_privkey) { + print "Potential private key found:" + found_privkey = 1 + } + print file ":" line_num ":" curr_line + print "" + } + } + } + + # Check for .env secrets (only if we have secrets to check) + if (num_secrets > 0) { + for (i = 1; i <= num_secrets; i++) { + value = secret_values[i] + pos = index(curr_line, value) + if (pos > 0) { + # Verify boundaries (not part of larger word) + before_ok = (pos == 1 || substr(curr_line, pos - 1, 1) !~ /[a-zA-Z0-9]/) + after_ok = (pos + length(value) - 1 == length(curr_line) || substr(curr_line, pos + length(value), 1) !~ /[a-zA-Z0-9]/) + if (before_ok && after_ok) { + disabled = (prev_line ~ comment || curr_line ~ comment) + if (!disabled) { + print "[" file ":" line_num "] Secret from .env file found (key: " secret_keys[value] ")" + } + } + } + } + } + + prev_line = curr_line +}' +EOFFUNC + + chmod +x "$TEMP_SCRIPT" + + # Get files to check (null-delimited for safety with spaces) + FILES_TO_CHECK=$(git diff --cached --name-only --diff-filter=ACM -z) + FILES_COUNT=$(echo "$FILES_TO_CHECK" | tr '\0' '\n' | wc -l | tr -d ' ') + + if [ "$FILES_COUNT" -eq 0 ]; then + echo 0 > "$SECRET_CHECK_EXIT" + rm -f "$TEMP_SCRIPT" + exit 0 + fi - # go through each (potential) match - while read -r LINE - do - # skip empty MATCHES - if [[ -z "$LINE" ]]; then - continue - fi + # Calculate optimal parallel jobs (I/O-bound operations) + if command -v sysctl >/dev/null 2>&1; then + NUM_CORES=$(sysctl -n hw.ncpu 2>/dev/null || echo "4") + elif command -v nproc >/dev/null 2>&1; then + NUM_CORES=$(nproc 2>/dev/null || echo "4") + else + NUM_CORES=4 + fi - LINENUMBER=$(echo "$LINE" | cut -d: -f2) + PARALLEL_JOBS=$((NUM_CORES * 4)) + [ "$PARALLEL_JOBS" -gt 32 ] && PARALLEL_JOBS=32 + [ "$PARALLEL_JOBS" -gt "$FILES_COUNT" ] && PARALLEL_JOBS="$FILES_COUNT" + [ "$PARALLEL_JOBS" -lt 1 ] && PARALLEL_JOBS=1 - # check if this is a known false positive marked by a comment in the code - if ! isKnownFalsePositiveMatch "$FILE" "$LINENUMBER"; then - echo "Potential private key found:" - echo "$LINE" - echo "" + # Process files in parallel + SECRET_RESULT="" + echo "$FILES_TO_CHECK" | xargs -0 -n 1 -P "$PARALLEL_JOBS" sh "$TEMP_SCRIPT" > "${SECRET_CHECK_OUTPUT}.results" 2>&1 + XARGS_EXIT=$? - fi + SECRET_RESULT=$(cat "${SECRET_CHECK_OUTPUT}.results" 2>/dev/null || true) + rm -f "${SECRET_CHECK_OUTPUT}.results" "$TEMP_SCRIPT" - done <<< "$(echo "$MATCHES")" + # Store result for later display (don't echo here to avoid interleaving) + if [ -n "$SECRET_RESULT" ]; then + echo "$SECRET_RESULT" + fi -} + # Determine exit code + EXIT_CODE=0 + if [ "$XARGS_EXIT" -ne 0 ]; then + EXIT_CODE=1 + elif echo "$SECRET_RESULT" | grep -q "Secret from .env file found"; then + printf '\033[36m%s\033[0m\n' "Warning: Secret value(s) from .env found. This code cannot be committed." + printf '\033[91m%s\033[0m\n' "Remove the secrets and try to commit again" + EXIT_CODE=1 + elif echo "$SECRET_RESULT" | grep -q "Potential private key found"; then + printf '\033[36m%s\033[0m\n' "Warning: Potential Ethereum private keys found" + printf '\033[91m%s\033[0m\n' "Check each match carefully before pushing to GitHub" + EXIT_CODE=0 # Warning only + fi -processGitDiff() { - echo "-------------------------------------- RESULTS: ---------------------------------------------––" - echo "" - - # Check for private keys and secrets in all added or modified FILES - git diff --cached --name-only --diff-filter=ACM | while IFS= read -r FILE; do - # Skip excluded paths - for EXCLUDED_PATH in "${EXCLUDED_PATHS[@]}"; do - if [[ "$FILE" == "$EXCLUDED_PATH"* ]]; then - continue 2 - fi - done - - # Check for secrets from .env file - RESULT_SECRET=$(doesFileContainDotEnvSecret "$FILE") - if [[ -n $RESULT_SECRET ]]; then - printRed "$RESULT_SECRET" - echo "" - fi + echo "$EXIT_CODE" > "$SECRET_CHECK_EXIT" +) > "$SECRET_CHECK_OUTPUT" 2>&1 & +SECRET_CHECK_PID=$! - # Check for potential private keys - RESULT_PRIVKEY=$(doesFileContainPotentialPrivateKey "$FILE") - if [[ -n $RESULT_PRIVKEY ]]; then - printYellow "$RESULT_PRIVKEY" - echo "" - fi - done - echo "---------------------------------------------------------------------------------------------––" -} +# Start forge build and lint-staged in parallel (they're independent initially) +FORGE_PID="" +LINT_STAGED_PID="" + +print_section "Pre-commit Checks" -checkGitDiffForSecretsAndPrivateKeys() { +# Start parallel tasks +if [ "$HAS_SOL_FILES" = "yes" ]; then + print_status "info" "Building contracts with forge..." + (forge build --skip test > "$FORGE_OUTPUT" 2>&1; echo $? > "$FORGE_EXIT") & + FORGE_PID=$! +else + print_status "skip" "Skipping forge build (no Solidity files changed)" + echo 0 > "$FORGE_EXIT" +fi - # process all files in git diff and search for secrets - local RESULT=$(processGitDiff) +if [ "$HAS_TYPE_FILES" = "yes" ]; then + print_status "info" "Running lint-staged (formatting & linting)..." + (bun lint-staged > "$LINT_STAGED_OUTPUT" 2>&1; echo $? > "$LINT_STAGED_EXIT") & + LINT_STAGED_PID=$! +else + print_status "skip" "Skipping lint-staged (no lintable files changed)" + echo 0 > "$LINT_STAGED_EXIT" +fi - # print the search results to console - if [[ -n $RESULT ]]; then - echo "$RESULT" +# Wait for forge build if it was started +if [ -n "$FORGE_PID" ]; then + if ! wait_and_check "$FORGE_PID" "$FORGE_EXIT" "$FORGE_OUTPUT" "Forge build failed. Aborting commit." "forge"; then + [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null + wait "$SECRET_CHECK_PID" 2>/dev/null + exit 1 fi + print_status "success" "Forge build completed" +fi - echo "" +# Run typechain generation (only if forge succeeded and we have Solidity files) +if [ "$HAS_SOL_FILES" = "yes" ]; then + print_status "info" "Generating TypeChain types..." + (bun typechain:incremental > "$TYPECHAIN_OUTPUT" 2>&1; echo $? > "$TYPECHAIN_EXIT") & + TYPECHAIN_PID=$! - # log a warning and prevent the commit if a secret was found - if [[ "$RESULT" == *"Secret from .env file found"* ]]; then + if ! wait_and_check "$TYPECHAIN_PID" "$TYPECHAIN_EXIT" "$TYPECHAIN_OUTPUT" "TypeChain generation failed. Aborting commit." "typechain"; then + [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null + wait "$SECRET_CHECK_PID" 2>/dev/null + exit 1 + fi + # Extract and show summary from typechain output (filter verbose lines) + TYPECHAIN_SUMMARY=$(grep -E "(Successfully|typings)" "$TYPECHAIN_OUTPUT" 2>/dev/null | grep -vE "^\$ " | head -1 || echo "TypeChain types generated") + print_status "success" "$TYPECHAIN_SUMMARY" +fi - echo "" - WARNING="Warning: Secret value(s) from .env found. This code cannot be committed." - printf '\033[36m%s\033[0m\n' "$WARNING" - printAdvise "abort" - echo "" +# TypeScript compilation check (only if both TS/JS files changed AND typechain was regenerated) +if [ "$HAS_TS_JS_FILES" = "yes" ] && [ "$HAS_SOL_FILES" = "yes" ]; then + print_status "info" "Checking TypeScript compilation..." + (bunx tsc-files --noEmit > "$TSC_OUTPUT" 2>&1; echo $? > "$TSC_EXIT") & + TSC_PID=$! + + if ! wait_and_check "$TSC_PID" "$TSC_EXIT" "$TSC_OUTPUT" "" "tsc"; then + [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null + wait "$SECRET_CHECK_PID" 2>/dev/null + printf '\n\033[31mTypeScript compilation failed.\033[0m\n' + printf 'This may indicate outdated TypeChain types.\n' + printf 'Please run \033[1mbun typechain\033[0m manually and fix any type errors before committing.\n\n' exit 1 fi + print_status "success" "TypeScript compilation passed" +fi - # log a warning and next steps if a potential private key was found (the commit will still be accepted) - if [[ "$RESULT" == *"Potential private key found"* ]]; then - printf '\033[36m%s\033[0m\n' "Warning: Potential Ethereum private keys found" - printAdvise "warning" - echo "" +# Wait for lint-staged if it was started +if [ -n "$LINT_STAGED_PID" ]; then + if ! wait_and_check "$LINT_STAGED_PID" "$LINT_STAGED_EXIT" "$LINT_STAGED_OUTPUT" "Lint-staged failed. Aborting commit." "lint-staged"; then + wait "$SECRET_CHECK_PID" 2>/dev/null + exit 1 fi + print_status "success" "Lint-staged completed" +fi - exit 0 -} +# Wait for secret checking and get results +print_status "info" "Checking for secrets and private keys..." +wait "$SECRET_CHECK_PID" 2>/dev/null +SECRET_CHECK_EXIT_CODE=$(cat "$SECRET_CHECK_EXIT" 2>/dev/null || echo "1") +SECRET_RESULT=$(cat "$SECRET_CHECK_OUTPUT" 2>/dev/null || true) + +if [ "$SECRET_CHECK_EXIT_CODE" -ne 0 ]; then + # Show errors if any + if [ -n "$SECRET_RESULT" ]; then + echo "$SECRET_RESULT" + fi + exit 1 +fi + +# Show secret check results if any issues found +if echo "$SECRET_RESULT" | grep -q "Secret from .env file found"; then + printf '\n\033[36m⚠ Warning: Secret value(s) from .env found. This code cannot be committed.\033[0m\n' + printf '\033[91mRemove the secrets and try to commit again\033[0m\n\n' + # Show the specific matches + echo "$SECRET_RESULT" | grep "Secret from .env file found" || true + exit 1 +elif echo "$SECRET_RESULT" | grep -q "Potential private key found"; then + printf '\n\033[33m⚠ Warning: Potential Ethereum private keys found\033[0m\n' + printf 'Check each match carefully before pushing to GitHub\n\n' + # Show the matches (limit to first few to avoid spam) + echo "$SECRET_RESULT" | grep -A 1 "Potential private key found" | head -10 || true + print_status "success" "Secret check completed (warnings only)" +else + print_status "success" "Secret check passed" +fi -checkGitDiffForSecretsAndPrivateKeys +printf '\n\033[1m━━━ All pre-commit checks passed! ━━━\033[0m\n' +exit 0 From 4ba512408e39e6cbe124e4167028b465036bc305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Fri, 12 Dec 2025 15:00:08 +0700 Subject: [PATCH 2/6] improved pre-commit runtime --- .husky/pre-commit | 581 ++++++++++++++++++++++++++++------------------ 1 file changed, 350 insertions(+), 231 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index f59a5b01a..b06e1d277 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,273 +1,392 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -echo "" -echo "Running 'forge build' and 'typechain generation'..." - -# Run forge build first (required for typechain) -echo "Building contracts with forge..." -forge build --skip test - -if [ $? -ne 0 ]; then - printf '\n%s\n\n' "Forge build failed. Aborting commit." - exit 1 -fi - -echo "Forge build completed successfully!" -echo "" - -# Run typechain generation (now that artifacts are available) -echo "Generating TypeChain types..." -bun typechain:incremental - -if [ $? -ne 0 ]; then - printf '\n%s\n\n' "TypeChain generation failed. Aborting commit." - exit 1 -fi - -echo "TypeChain generation completed successfully!" -echo "" - -# Run TypeScript compilation to catch any type errors from outdated typings -echo "Running TypeScript compilation to check for type errors..." -bunx tsc-files --noEmit - -if [ $? -ne 0 ]; then - printf '\n%s\n' "TypeScript compilation failed. This may indicate outdated TypeChain types." - printf '%s\n\n' "Please run 'bun typechain' manually and fix any type errors before committing." - exit 1 +# Early exit: Check if there are any staged files +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM) +if [ -z "$STAGED_FILES" ]; then + echo "No files staged for commit. Skipping pre-commit checks." + exit 0 fi -echo "" -echo "TypeScript compilation passed - all types are up to date!" -echo "" - -echo "" -echo "Running 'bun lint-staged' now:" -bun lint-staged - -echo "" -echo "" -echo "now checking for .env secrets and private keys accidentally being committed to Github" -echo " > any 64-byte hex string will be identified as potential private key" -echo " > disable false positive matches by commenting '[pre-commit-checker: not a secret]' in same line or line above" -echo " > execution might take a while depending on the size of your git diff " -echo " > logs will only be shown once task is completed" -echo "" - -DISABLE_WITH_COMMENT="pre-commit-checker: not a secret" -# Regex pattern to identify potential Ethereum private keys (64 hexadecimal characters) -ETH_PRIVATE_KEY_PATTERN="\b[a-fA-F0-9]{64}\b" - -# List of known false positive values that should not be flagged as secrets -KNOWN_FALSE_POSITIVES=( - "true" - "false" - "none" - "" - "verifyContract" -) - -# List of paths to exclude from secret checks -EXCLUDED_PATHS=( - "deployments/_deployments_log_file.json" - "config/networks.json" - "lib/" - "safe/cancun/out/" - "safe/london/out/" - "bun.lock" - ".bun/" -) - -# Load secrets from .env file -if [ -f ".env" ]; then - ENV_SECRETS=$(grep -v '^#' .env | sed 's/#.*//' | grep -v '^\s*$' | sed 's/ *$//') -else - echo ".env file not found" - ENV_SECRETS="" -fi +# Determine what types of files changed for conditional execution (optimized single pass) +HAS_SOL_FILES="no" +HAS_TS_JS_FILES="no" +HAS_TYPE_FILES="no" + +echo "$STAGED_FILES" | grep -qE '\.sol$' && HAS_SOL_FILES="yes" && HAS_TYPE_FILES="yes" +echo "$STAGED_FILES" | grep -qE '\.(ts|js|tsx)$' && HAS_TS_JS_FILES="yes" && HAS_TYPE_FILES="yes" + +# Create temp directory for parallel execution +TEMP_DIR=$(mktemp -d) +trap "rm -rf '$TEMP_DIR' 2>/dev/null" EXIT INT TERM + +FORGE_OUTPUT="$TEMP_DIR/forge.out" +FORGE_EXIT="$TEMP_DIR/forge.exit" +TYPECHAIN_OUTPUT="$TEMP_DIR/typechain.out" +TYPECHAIN_EXIT="$TEMP_DIR/typechain.exit" +TSC_OUTPUT="$TEMP_DIR/tsc.out" +TSC_EXIT="$TEMP_DIR/tsc.exit" +LINT_STAGED_OUTPUT="$TEMP_DIR/lint-staged.out" +LINT_STAGED_EXIT="$TEMP_DIR/lint-staged.exit" +SECRET_CHECK_OUTPUT="$TEMP_DIR/secret-check.out" +SECRET_CHECK_EXIT="$TEMP_DIR/secret-check.exit" + +# Helper function to print section header +print_section() { + printf '\n\033[1m━━━ %s ━━━\033[0m\n' "$1" +} -printRed() { - local MESSAGE="$1" - printf '\033[31m%s\033[0m\n' "$MESSAGE" +# Helper function to print status +print_status() { + local status=$1 + local message=$2 + if [ "$status" = "success" ]; then + printf '\033[32m✓\033[0m %s\n' "$message" + elif [ "$status" = "skip" ]; then + printf '\033[33m⊘\033[0m %s\n' "$message" + elif [ "$status" = "error" ]; then + printf '\033[31m✗\033[0m %s\n' "$message" + else + printf ' %s\n' "$message" + fi } -printYellow() { - local MESSAGE="$1" - printf '\033[33m%s\033[0m\n' "$MESSAGE" + +# Helper function to filter verbose output +filter_output() { + local tool=$1 + local content=$2 + + case "$tool" in + forge) + # Only show errors, suppress compilation details + echo "$content" | grep -E "(Error|Failed|error|failed)" || true + ;; + typechain) + # Show only errors and summary, suppress verbose output + echo "$content" | grep -vE "^\$ |^Resolving|^Resolved|^Saved|^Cleaned|^Cleaned duplicate" | grep -E "(Successfully|Error|error|Failed|failed)" || true + ;; + lint-staged) + # Suppress verbose lint-staged output, keep only errors and important messages + echo "$content" | grep -vE "^\[(STARTED|COMPLETED)\]" | grep -vE "^Preparing lint-staged" | grep -vE "^Running tasks" | grep -vE "^Applying modifications" | grep -vE "^Cleaning up" | grep -E "(error|Error|failed|Failed|warning|Warning)" || true + ;; + tsc) + # Show only errors + echo "$content" | grep -vE "^$" | head -20 || true + ;; + *) + echo "$content" + ;; + esac } -printAdvise() { - local ACTION="$1" +# Helper function to wait for PIDs and exit on failure +wait_and_check() { + local pid=$1 + local exit_file=$2 + local output_file=$3 + local error_msg=$4 + local tool_name=$5 - echo "" - printf '\033[91m%s\033[0m\n' "NEXT STEPS" - if [ "$ACTION" = "abort" ]; then - printf '\033[91m%s\033[0m\n' "Remove the secrets and try to commit again" - else - printf '\033[91m%s\033[0m\n' "Check each match carefully and make sure that no sensitive information is being committed" - printf '\033[91m%s\033[0m\n' "If it did happen, undo the commit with 'git reset --soft HEAD~1', remove the secret(s) and commit again." - printf '\033[91m%s\033[0m\n' "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! BEFORE PUSHING TO GITHUB !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + if [ -z "$pid" ]; then + return 0 fi -} -# checks if the given line number or the line above contain a comment to disable the check -isKnownFalsePositiveMatch() { - local FILE="$1" - local LINE_NUMBER="$2" - - if [ "$LINE_NUMBER" -gt 1 ]; then - local PREV_LINE=$(sed "$((LINE_NUMBER - 1))q;d" "$FILE") - local CURR_LINE=$(sed "${LINE_NUMBER}q;d" "$FILE") - - # check if comment is present in line above - if echo "$PREV_LINE" | grep -q "$DISABLE_WITH_COMMENT"; then - return 0 - # check if comment is present in current line - elif echo "$CURR_LINE" | grep -q "$DISABLE_WITH_COMMENT"; then - return 0 - fi - # special handlng if line number is first line (then only check this line) - elif [ "$LINE_NUMBER" -eq 1 ]; then - local CURR_LINE=$(sed "${LINE_NUMBER}q;d" "$FILE") + wait "$pid" 2>/dev/null + local exit_code=$(cat "$exit_file" 2>/dev/null || echo "1") - if echo "$CURR_LINE" | grep -q "$DISABLE_WITH_COMMENT"; then - return 0 + if [ "$exit_code" -ne 0 ] && [ -f "$output_file" ]; then + # Show filtered output only on errors + if [ -n "$tool_name" ]; then + filter_output "$tool_name" "$(cat "$output_file")" + else + cat "$output_file" fi fi - return 1 + if [ "$exit_code" -ne 0 ]; then + [ -n "$error_msg" ] && printf '\n\033[31m%s\033[0m\n\n' "$error_msg" + return 1 + fi + return 0 } -# checks if a file contains any of the secrets in .env -doesFileContainDotEnvSecret() { - local FILE="$1" - # iterate through all secrets - for SECRET in $ENV_SECRETS; do - # extract key and value - local VALUE=$(echo "$SECRET" | cut -d '=' -f 2- | sed -e 's/^["'\''"]*//' -e 's/["'\''"]*$//') - local KEY=$(echo "$SECRET" | cut -d '=' -f 1) - - # skip empty values and known false positives - local IS_FALSE_POSITIVE=false - for FALSE_POSITIVE in "${KNOWN_FALSE_POSITIVES[@]}"; do - if [ "$VALUE" = "$FALSE_POSITIVE" ]; then - IS_FALSE_POSITIVE=true - break - fi - done - - if [ -z "$VALUE" ] || [ "$IS_FALSE_POSITIVE" = true ]; then - continue - fi +# Start secret checking immediately (no dependencies, runs on staged file content to avoid race conditions) +( + # Suppress echo in background process - we'll show it when we wait + : - # go through file line by line - grep -nH "$VALUE" "$FILE" | while IFS= read -r LINE; do - LINE_CONTENT=$(echo "$LINE" | cut -d: -f2-) - - # check if the FILE contains the SECRET value - if [ "$LINE_CONTENT" != "" ] && echo "$LINE_CONTENT" | grep -q "$VALUE"; then - # match found - LINE_NUMBER=$(echo "$LINE" | cut -d: -f2) - # check if this is a known false positive marked by a comment in the code - if ! isKnownFalsePositiveMatch "$FILE" "$LINE_NUMBER"; then - # print match - echo "[$FILE:$LINE_NUMBER] Secret from .env file found (key: $KEY)" - fi - fi - done - done -} + DISABLE_WITH_COMMENT="pre-commit-checker: not a secret" + ETH_PRIVATE_KEY_PATTERN="[a-fA-F0-9]{64}" + EXCLUDED_PATHS="deployments/_deployments_log_file.json config/networks.json lib/ safe/cancun/out/ safe/london/out/ bun.lock .bun/" -# checks if a file contains a potential private key (=> a 64 hex string) -doesFileContainPotentialPrivateKey() { - local FILE="$1" + # Parse .env file once (optimized) + ENV_SECRETS="" + if [ -f ".env" ]; then + ENV_SECRETS=$(awk -F= '/^[^#]/ && NF >= 2 { + gsub(/^[ \t]*["'\''"]*|["'\''"]*[ \t]*$/, "", $2) + if ($2 != "" && $2 != "true" && $2 != "false" && $2 != "none" && $2 != "verifyContract") { + print $1 "=" $2 + } + }' .env) + fi - # check file content for matches with private key regEx - local MATCHES=$(grep -E -nH "$ETH_PRIVATE_KEY_PATTERN" "$FILE") + # Optimized secret checking script - single pass for all checks + TEMP_SCRIPT=$(mktemp) + cat > "$TEMP_SCRIPT" << EOFFUNC +#!/bin/sh +FILE="\$1" +DISABLE_COMMENT="$DISABLE_WITH_COMMENT" +PRIVKEY_PATTERN="$ETH_PRIVATE_KEY_PATTERN" +ENV_SECRETS="$ENV_SECRETS" +EXCLUDED_PATHS="$EXCLUDED_PATHS" + +# Fast exclusion check +for EXCLUDED in \$EXCLUDED_PATHS; do + case "\$FILE" in + \$EXCLUDED*) exit 0 ;; + esac +done + +# Get staged file content (avoids race conditions with lint-staged modifications) +FILE_CONTENT=\$(git show ":\$FILE" 2>/dev/null) +[ -z "\$FILE_CONTENT" ] && exit 0 + +# Combined single-pass check for both private keys and secrets (maximum efficiency) +echo "\$FILE_CONTENT" | awk -v file="\$FILE" -v comment="\$DISABLE_COMMENT" -v env_secrets="\$ENV_SECRETS" ' +BEGIN { + found_privkey = 0 + prev_line = "" + + # Build secret lookup array + if (env_secrets != "") { + n = split(env_secrets, secrets, "\n") + for (i = 1; i <= n; i++) { + if (secrets[i] != "") { + eq_pos = index(secrets[i], "=") + if (eq_pos > 0) { + key = substr(secrets[i], 1, eq_pos - 1) + value = substr(secrets[i], eq_pos + 1) + gsub(/^[ \t]*["'\''"]+|["'\''"]*[ \t]*$/, "", value) + if (value != "" && length(value) > 3) { + secret_values[++num_secrets] = value + secret_keys[value] = key + } + } + } + } + } +} +{ + line_num = NR + curr_line = \$0 + + # Check for private keys (64 hex chars as standalone token) + if (match(curr_line, /(^|[^0-9a-fA-F])[0-9a-fA-F]{64}([^0-9a-fA-F]|$)/)) { + match_pos = RSTART + if (substr(curr_line, match_pos, 1) !~ /[0-9a-fA-F]/) { + match_pos = match_pos + 1 + } + # Verify it's exactly 64, not part of longer hex + if (substr(curr_line, match_pos + 64, 1) !~ /[0-9a-fA-F]/) { + disabled = (prev_line ~ comment || curr_line ~ comment) + if (!disabled) { + if (!found_privkey) { + print "Potential private key found:" + found_privkey = 1 + } + print file ":" line_num ":" curr_line + print "" + } + } + } + + # Check for .env secrets (only if we have secrets to check) + if (num_secrets > 0) { + for (i = 1; i <= num_secrets; i++) { + value = secret_values[i] + pos = index(curr_line, value) + if (pos > 0) { + # Verify boundaries (not part of larger word) + before_ok = (pos == 1 || substr(curr_line, pos - 1, 1) !~ /[a-zA-Z0-9]/) + after_ok = (pos + length(value) - 1 == length(curr_line) || substr(curr_line, pos + length(value), 1) !~ /[a-zA-Z0-9]/) + if (before_ok && after_ok) { + disabled = (prev_line ~ comment || curr_line ~ comment) + if (!disabled) { + print "[" file ":" line_num "] Secret from .env file found (key: " secret_keys[value] ")" + } + } + } + } + } + + prev_line = curr_line +}' +EOFFUNC + + chmod +x "$TEMP_SCRIPT" + + # Get files to check (null-delimited for safety with spaces) + FILES_TO_CHECK=$(git diff --cached --name-only --diff-filter=ACM -z) + FILES_COUNT=$(echo "$FILES_TO_CHECK" | tr '\0' '\n' | wc -l | tr -d ' ') + + if [ "$FILES_COUNT" -eq 0 ]; then + echo 0 > "$SECRET_CHECK_EXIT" + rm -f "$TEMP_SCRIPT" + exit 0 + fi - # go through each (potential) match - while read -r LINE - do - # skip empty MATCHES - if [[ -z "$LINE" ]]; then - continue - fi + # Calculate optimal parallel jobs (I/O-bound operations) + if command -v sysctl >/dev/null 2>&1; then + NUM_CORES=$(sysctl -n hw.ncpu 2>/dev/null || echo "4") + elif command -v nproc >/dev/null 2>&1; then + NUM_CORES=$(nproc 2>/dev/null || echo "4") + else + NUM_CORES=4 + fi - LINENUMBER=$(echo "$LINE" | cut -d: -f2) + PARALLEL_JOBS=$((NUM_CORES * 4)) + [ "$PARALLEL_JOBS" -gt 32 ] && PARALLEL_JOBS=32 + [ "$PARALLEL_JOBS" -gt "$FILES_COUNT" ] && PARALLEL_JOBS="$FILES_COUNT" + [ "$PARALLEL_JOBS" -lt 1 ] && PARALLEL_JOBS=1 - # check if this is a known false positive marked by a comment in the code - if ! isKnownFalsePositiveMatch "$FILE" "$LINENUMBER"; then - echo "Potential private key found:" - echo "$LINE" - echo "" + # Process files in parallel + SECRET_RESULT="" + echo "$FILES_TO_CHECK" | xargs -0 -n 1 -P "$PARALLEL_JOBS" sh "$TEMP_SCRIPT" > "${SECRET_CHECK_OUTPUT}.results" 2>&1 + XARGS_EXIT=$? - fi + SECRET_RESULT=$(cat "${SECRET_CHECK_OUTPUT}.results" 2>/dev/null || true) + rm -f "${SECRET_CHECK_OUTPUT}.results" "$TEMP_SCRIPT" - done <<< "$(echo "$MATCHES")" + # Store result for later display (don't echo here to avoid interleaving) + if [ -n "$SECRET_RESULT" ]; then + echo "$SECRET_RESULT" + fi -} + # Determine exit code + EXIT_CODE=0 + if [ "$XARGS_EXIT" -ne 0 ]; then + EXIT_CODE=1 + elif echo "$SECRET_RESULT" | grep -q "Secret from .env file found"; then + printf '\033[36m%s\033[0m\n' "Warning: Secret value(s) from .env found. This code cannot be committed." + printf '\033[91m%s\033[0m\n' "Remove the secrets and try to commit again" + EXIT_CODE=1 + elif echo "$SECRET_RESULT" | grep -q "Potential private key found"; then + printf '\033[36m%s\033[0m\n' "Warning: Potential Ethereum private keys found" + printf '\033[91m%s\033[0m\n' "Check each match carefully before pushing to GitHub" + EXIT_CODE=0 # Warning only + fi -processGitDiff() { - echo "-------------------------------------- RESULTS: ---------------------------------------------––" - echo "" - - # Check for private keys and secrets in all added or modified FILES - git diff --cached --name-only --diff-filter=ACM | while IFS= read -r FILE; do - # Skip excluded paths - for EXCLUDED_PATH in "${EXCLUDED_PATHS[@]}"; do - if [[ "$FILE" == "$EXCLUDED_PATH"* ]]; then - continue 2 - fi - done - - # Check for secrets from .env file - RESULT_SECRET=$(doesFileContainDotEnvSecret "$FILE") - if [[ -n $RESULT_SECRET ]]; then - printRed "$RESULT_SECRET" - echo "" - fi + echo "$EXIT_CODE" > "$SECRET_CHECK_EXIT" +) > "$SECRET_CHECK_OUTPUT" 2>&1 & +SECRET_CHECK_PID=$! - # Check for potential private keys - RESULT_PRIVKEY=$(doesFileContainPotentialPrivateKey "$FILE") - if [[ -n $RESULT_PRIVKEY ]]; then - printYellow "$RESULT_PRIVKEY" - echo "" - fi - done - echo "---------------------------------------------------------------------------------------------––" -} +# Start forge build and lint-staged in parallel (they're independent initially) +FORGE_PID="" +LINT_STAGED_PID="" + +print_section "Pre-commit Checks" -checkGitDiffForSecretsAndPrivateKeys() { +# Start parallel tasks +if [ "$HAS_SOL_FILES" = "yes" ]; then + print_status "info" "Building contracts with forge..." + (forge build --skip test > "$FORGE_OUTPUT" 2>&1; echo $? > "$FORGE_EXIT") & + FORGE_PID=$! +else + print_status "skip" "Skipping forge build (no Solidity files changed)" + echo 0 > "$FORGE_EXIT" +fi - # process all files in git diff and search for secrets - local RESULT=$(processGitDiff) +if [ "$HAS_TYPE_FILES" = "yes" ]; then + print_status "info" "Running lint-staged (formatting & linting)..." + (bun lint-staged > "$LINT_STAGED_OUTPUT" 2>&1; echo $? > "$LINT_STAGED_EXIT") & + LINT_STAGED_PID=$! +else + print_status "skip" "Skipping lint-staged (no lintable files changed)" + echo 0 > "$LINT_STAGED_EXIT" +fi - # print the search results to console - if [[ -n $RESULT ]]; then - echo "$RESULT" +# Wait for forge build if it was started +if [ -n "$FORGE_PID" ]; then + if ! wait_and_check "$FORGE_PID" "$FORGE_EXIT" "$FORGE_OUTPUT" "Forge build failed. Aborting commit." "forge"; then + [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null + wait "$SECRET_CHECK_PID" 2>/dev/null + exit 1 fi + print_status "success" "Forge build completed" +fi - echo "" +# Run typechain generation (only if forge succeeded and we have Solidity files) +if [ "$HAS_SOL_FILES" = "yes" ]; then + print_status "info" "Generating TypeChain types..." + (bun typechain:incremental > "$TYPECHAIN_OUTPUT" 2>&1; echo $? > "$TYPECHAIN_EXIT") & + TYPECHAIN_PID=$! - # log a warning and prevent the commit if a secret was found - if [[ "$RESULT" == *"Secret from .env file found"* ]]; then + if ! wait_and_check "$TYPECHAIN_PID" "$TYPECHAIN_EXIT" "$TYPECHAIN_OUTPUT" "TypeChain generation failed. Aborting commit." "typechain"; then + [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null + wait "$SECRET_CHECK_PID" 2>/dev/null + exit 1 + fi + # Extract and show summary from typechain output (filter verbose lines) + TYPECHAIN_SUMMARY=$(grep -E "(Successfully|typings)" "$TYPECHAIN_OUTPUT" 2>/dev/null | grep -vE "^\$ " | head -1 || echo "TypeChain types generated") + print_status "success" "$TYPECHAIN_SUMMARY" +fi - echo "" - WARNING="Warning: Secret value(s) from .env found. This code cannot be committed." - printf '\033[36m%s\033[0m\n' "$WARNING" - printAdvise "abort" - echo "" +# TypeScript compilation check (only if both TS/JS files changed AND typechain was regenerated) +if [ "$HAS_TS_JS_FILES" = "yes" ] && [ "$HAS_SOL_FILES" = "yes" ]; then + print_status "info" "Checking TypeScript compilation..." + (bunx tsc-files --noEmit > "$TSC_OUTPUT" 2>&1; echo $? > "$TSC_EXIT") & + TSC_PID=$! + + if ! wait_and_check "$TSC_PID" "$TSC_EXIT" "$TSC_OUTPUT" "" "tsc"; then + [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null + wait "$SECRET_CHECK_PID" 2>/dev/null + printf '\n\033[31mTypeScript compilation failed.\033[0m\n' + printf 'This may indicate outdated TypeChain types.\n' + printf 'Please run \033[1mbun typechain\033[0m manually and fix any type errors before committing.\n\n' exit 1 fi + print_status "success" "TypeScript compilation passed" +fi - # log a warning and next steps if a potential private key was found (the commit will still be accepted) - if [[ "$RESULT" == *"Potential private key found"* ]]; then - printf '\033[36m%s\033[0m\n' "Warning: Potential Ethereum private keys found" - printAdvise "warning" - echo "" +# Wait for lint-staged if it was started +if [ -n "$LINT_STAGED_PID" ]; then + if ! wait_and_check "$LINT_STAGED_PID" "$LINT_STAGED_EXIT" "$LINT_STAGED_OUTPUT" "Lint-staged failed. Aborting commit." "lint-staged"; then + wait "$SECRET_CHECK_PID" 2>/dev/null + exit 1 fi + print_status "success" "Lint-staged completed" +fi - exit 0 -} +# Wait for secret checking and get results +print_status "info" "Checking for secrets and private keys..." +wait "$SECRET_CHECK_PID" 2>/dev/null +SECRET_CHECK_EXIT_CODE=$(cat "$SECRET_CHECK_EXIT" 2>/dev/null || echo "1") +SECRET_RESULT=$(cat "$SECRET_CHECK_OUTPUT" 2>/dev/null || true) + +if [ "$SECRET_CHECK_EXIT_CODE" -ne 0 ]; then + # Show errors if any + if [ -n "$SECRET_RESULT" ]; then + echo "$SECRET_RESULT" + fi + exit 1 +fi + +# Show secret check results if any issues found +if echo "$SECRET_RESULT" | grep -q "Secret from .env file found"; then + printf '\n\033[36m⚠ Warning: Secret value(s) from .env found. This code cannot be committed.\033[0m\n' + printf '\033[91mRemove the secrets and try to commit again\033[0m\n\n' + # Show the specific matches + echo "$SECRET_RESULT" | grep "Secret from .env file found" || true + exit 1 +elif echo "$SECRET_RESULT" | grep -q "Potential private key found"; then + printf '\n\033[33m⚠ Warning: Potential Ethereum private keys found\033[0m\n' + printf 'Check each match carefully before pushing to GitHub\n\n' + # Show the matches (limit to first few to avoid spam) + echo "$SECRET_RESULT" | grep -A 1 "Potential private key found" | head -10 || true + print_status "success" "Secret check completed (warnings only)" +else + print_status "success" "Secret check passed" +fi -checkGitDiffForSecretsAndPrivateKeys +printf '\n\033[1m━━━ All pre-commit checks passed! ━━━\033[0m\n' +exit 0 From 4dfe54bf613533fe0ba278001795403120a91c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Fri, 12 Dec 2025 15:10:13 +0700 Subject: [PATCH 3/6] some fixes and improvements --- .husky/_/.gitignore | 3 + .husky/_/husky.sh | 37 +++++ .husky/pre-commit | 386 +++++++++++++++++++++++--------------------- 3 files changed, 238 insertions(+), 188 deletions(-) create mode 100644 .husky/_/.gitignore create mode 100644 .husky/_/husky.sh diff --git a/.husky/_/.gitignore b/.husky/_/.gitignore new file mode 100644 index 000000000..614400ca1 --- /dev/null +++ b/.husky/_/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!husky.sh diff --git a/.husky/_/husky.sh b/.husky/_/husky.sh new file mode 100644 index 000000000..f6111ac9b --- /dev/null +++ b/.husky/_/husky.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env sh +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 + husky_shell="${HUSKY_SHELL:-sh}" + "$husky_shell" -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/pre-commit b/.husky/pre-commit index b06e1d277..36594f920 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,5 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash +export HUSKY_SHELL="${HUSKY_SHELL:-bash}" . "$(dirname -- "$0")/_/husky.sh" # Early exit: Check if there are any staged files @@ -8,6 +9,9 @@ if [ -z "$STAGED_FILES" ]; then exit 0 fi +GIT_ROOT=$(git rev-parse --show-toplevel) +cd "$GIT_ROOT" || exit 1 + # Determine what types of files changed for conditional execution (optimized single pass) HAS_SOL_FILES="no" HAS_TS_JS_FILES="no" @@ -28,8 +32,31 @@ TSC_OUTPUT="$TEMP_DIR/tsc.out" TSC_EXIT="$TEMP_DIR/tsc.exit" LINT_STAGED_OUTPUT="$TEMP_DIR/lint-staged.out" LINT_STAGED_EXIT="$TEMP_DIR/lint-staged.exit" -SECRET_CHECK_OUTPUT="$TEMP_DIR/secret-check.out" -SECRET_CHECK_EXIT="$TEMP_DIR/secret-check.exit" + +DISABLE_WITH_COMMENT="pre-commit-checker: not a secret" +ETH_PRIVATE_KEY_PATTERN='(^|[^0-9a-fA-F])[0-9a-fA-F]{64}([^0-9a-fA-F]|$)' +KNOWN_FALSE_POSITIVES=( + "true" + "false" + "none" + "" + "verifyContract" +) +EXCLUDED_PATHS=( + "deployments/_deployments_log_file.json" + "config/networks.json" + "lib/" + "safe/cancun/out/" + "safe/london/out/" + "bun.lock" + ".bun/" +) + +ENV_SECRET_KEYS=() +ENV_SECRET_VALUES=() +SECRET_FOUND=0 +PRIVATE_KEY_FOUND=0 +SECRET_RESULTS="" # Helper function to print section header print_section() { @@ -42,10 +69,14 @@ print_status() { local message=$2 if [ "$status" = "success" ]; then printf '\033[32m✓\033[0m %s\n' "$message" + elif [ "$status" = "warning" ]; then + # Print the full line in yellow (easy to notice) + printf '\033[33m⚠ %s\033[0m\n' "$message" elif [ "$status" = "skip" ]; then printf '\033[33m⊘\033[0m %s\n' "$message" elif [ "$status" = "error" ]; then - printf '\033[31m✗\033[0m %s\n' "$message" + # Print the full line in red (easy to notice) + printf '\033[31m✗ %s\033[0m\n' "$message" else printf ' %s\n' "$message" fi @@ -110,175 +141,159 @@ wait_and_check() { return 0 } -# Start secret checking immediately (no dependencies, runs on staged file content to avoid race conditions) -( - # Suppress echo in background process - we'll show it when we wait - : - - DISABLE_WITH_COMMENT="pre-commit-checker: not a secret" - ETH_PRIVATE_KEY_PATTERN="[a-fA-F0-9]{64}" - EXCLUDED_PATHS="deployments/_deployments_log_file.json config/networks.json lib/ safe/cancun/out/ safe/london/out/ bun.lock .bun/" - - # Parse .env file once (optimized) - ENV_SECRETS="" - if [ -f ".env" ]; then - ENV_SECRETS=$(awk -F= '/^[^#]/ && NF >= 2 { - gsub(/^[ \t]*["'\''"]*|["'\''"]*[ \t]*$/, "", $2) - if ($2 != "" && $2 != "true" && $2 != "false" && $2 != "none" && $2 != "verifyContract") { - print $1 "=" $2 - } - }' .env) - fi +is_excluded_path() { + local FILE="$1" - # Optimized secret checking script - single pass for all checks - TEMP_SCRIPT=$(mktemp) - cat > "$TEMP_SCRIPT" << EOFFUNC -#!/bin/sh -FILE="\$1" -DISABLE_COMMENT="$DISABLE_WITH_COMMENT" -PRIVKEY_PATTERN="$ETH_PRIVATE_KEY_PATTERN" -ENV_SECRETS="$ENV_SECRETS" -EXCLUDED_PATHS="$EXCLUDED_PATHS" - -# Fast exclusion check -for EXCLUDED in \$EXCLUDED_PATHS; do - case "\$FILE" in - \$EXCLUDED*) exit 0 ;; - esac -done - -# Get staged file content (avoids race conditions with lint-staged modifications) -FILE_CONTENT=\$(git show ":\$FILE" 2>/dev/null) -[ -z "\$FILE_CONTENT" ] && exit 0 - -# Combined single-pass check for both private keys and secrets (maximum efficiency) -echo "\$FILE_CONTENT" | awk -v file="\$FILE" -v comment="\$DISABLE_COMMENT" -v env_secrets="\$ENV_SECRETS" ' -BEGIN { - found_privkey = 0 - prev_line = "" - - # Build secret lookup array - if (env_secrets != "") { - n = split(env_secrets, secrets, "\n") - for (i = 1; i <= n; i++) { - if (secrets[i] != "") { - eq_pos = index(secrets[i], "=") - if (eq_pos > 0) { - key = substr(secrets[i], 1, eq_pos - 1) - value = substr(secrets[i], eq_pos + 1) - gsub(/^[ \t]*["'\''"]+|["'\''"]*[ \t]*$/, "", value) - if (value != "" && length(value) > 3) { - secret_values[++num_secrets] = value - secret_keys[value] = key - } - } - } - } - } + for EXCLUDED_PATH in "${EXCLUDED_PATHS[@]}"; do + if [[ "$FILE" == "$EXCLUDED_PATH"* ]]; then + return 0 + fi + done + + return 1 } -{ - line_num = NR - curr_line = \$0 - - # Check for private keys (64 hex chars as standalone token) - if (match(curr_line, /(^|[^0-9a-fA-F])[0-9a-fA-F]{64}([^0-9a-fA-F]|$)/)) { - match_pos = RSTART - if (substr(curr_line, match_pos, 1) !~ /[0-9a-fA-F]/) { - match_pos = match_pos + 1 - } - # Verify it's exactly 64, not part of longer hex - if (substr(curr_line, match_pos + 64, 1) !~ /[0-9a-fA-F]/) { - disabled = (prev_line ~ comment || curr_line ~ comment) - if (!disabled) { - if (!found_privkey) { - print "Potential private key found:" - found_privkey = 1 - } - print file ":" line_num ":" curr_line - print "" - } - } - } - - # Check for .env secrets (only if we have secrets to check) - if (num_secrets > 0) { - for (i = 1; i <= num_secrets; i++) { - value = secret_values[i] - pos = index(curr_line, value) - if (pos > 0) { - # Verify boundaries (not part of larger word) - before_ok = (pos == 1 || substr(curr_line, pos - 1, 1) !~ /[a-zA-Z0-9]/) - after_ok = (pos + length(value) - 1 == length(curr_line) || substr(curr_line, pos + length(value), 1) !~ /[a-zA-Z0-9]/) - if (before_ok && after_ok) { - disabled = (prev_line ~ comment || curr_line ~ comment) - if (!disabled) { - print "[" file ":" line_num "] Secret from .env file found (key: " secret_keys[value] ")" - } - } - } - } - } - - prev_line = curr_line -}' -EOFFUNC - - chmod +x "$TEMP_SCRIPT" - - # Get files to check (null-delimited for safety with spaces) - FILES_TO_CHECK=$(git diff --cached --name-only --diff-filter=ACM -z) - FILES_COUNT=$(echo "$FILES_TO_CHECK" | tr '\0' '\n' | wc -l | tr -d ' ') - - if [ "$FILES_COUNT" -eq 0 ]; then - echo 0 > "$SECRET_CHECK_EXIT" - rm -f "$TEMP_SCRIPT" - exit 0 - fi - # Calculate optimal parallel jobs (I/O-bound operations) - if command -v sysctl >/dev/null 2>&1; then - NUM_CORES=$(sysctl -n hw.ncpu 2>/dev/null || echo "4") - elif command -v nproc >/dev/null 2>&1; then - NUM_CORES=$(nproc 2>/dev/null || echo "4") - else - NUM_CORES=4 - fi +is_known_false_positive_match() { + local FILE="$1" + local LINE_NUMBER="$2" - PARALLEL_JOBS=$((NUM_CORES * 4)) - [ "$PARALLEL_JOBS" -gt 32 ] && PARALLEL_JOBS=32 - [ "$PARALLEL_JOBS" -gt "$FILES_COUNT" ] && PARALLEL_JOBS="$FILES_COUNT" - [ "$PARALLEL_JOBS" -lt 1 ] && PARALLEL_JOBS=1 + local PREV_LINE="" + local CURR_LINE="" - # Process files in parallel - SECRET_RESULT="" - echo "$FILES_TO_CHECK" | xargs -0 -n 1 -P "$PARALLEL_JOBS" sh "$TEMP_SCRIPT" > "${SECRET_CHECK_OUTPUT}.results" 2>&1 - XARGS_EXIT=$? + if [ "$LINE_NUMBER" -gt 1 ]; then + PREV_LINE=$(sed -n "$((LINE_NUMBER - 1))p" "$FILE") + fi - SECRET_RESULT=$(cat "${SECRET_CHECK_OUTPUT}.results" 2>/dev/null || true) - rm -f "${SECRET_CHECK_OUTPUT}.results" "$TEMP_SCRIPT" + CURR_LINE=$(sed -n "${LINE_NUMBER}p" "$FILE") - # Store result for later display (don't echo here to avoid interleaving) - if [ -n "$SECRET_RESULT" ]; then - echo "$SECRET_RESULT" + if printf '%s\n%s\n' "$PREV_LINE" "$CURR_LINE" | grep -q "$DISABLE_WITH_COMMENT"; then + return 0 fi - # Determine exit code - EXIT_CODE=0 - if [ "$XARGS_EXIT" -ne 0 ]; then - EXIT_CODE=1 - elif echo "$SECRET_RESULT" | grep -q "Secret from .env file found"; then - printf '\033[36m%s\033[0m\n' "Warning: Secret value(s) from .env found. This code cannot be committed." - printf '\033[91m%s\033[0m\n' "Remove the secrets and try to commit again" - EXIT_CODE=1 - elif echo "$SECRET_RESULT" | grep -q "Potential private key found"; then - printf '\033[36m%s\033[0m\n' "Warning: Potential Ethereum private keys found" - printf '\033[91m%s\033[0m\n' "Check each match carefully before pushing to GitHub" - EXIT_CODE=0 # Warning only + return 1 +} + +prepare_env_secrets() { + local ENV_FILE="$GIT_ROOT/.env" + local ENV_SECRETS_FILE="$TEMP_DIR/env_secrets" + + if [ ! -f "$ENV_FILE" ]; then + return fi - echo "$EXIT_CODE" > "$SECRET_CHECK_EXIT" -) > "$SECRET_CHECK_OUTPUT" 2>&1 & -SECRET_CHECK_PID=$! + grep -v '^#' "$ENV_FILE" | sed 's/#.*//' | grep -v '^[[:space:]]*$' | sed 's/[[:space:]]*$//' > "$ENV_SECRETS_FILE" + + while IFS= read -r SECRET_LINE || [ -n "$SECRET_LINE" ]; do + local KEY=${SECRET_LINE%%=*} + local VALUE=${SECRET_LINE#*=} + + KEY=$(printf '%s' "$KEY" | sed 's/[[:space:]]*$//') + VALUE=$(printf '%s' "$VALUE" | sed -e 's/^["'\''"]*//' -e 's/["'\''"]*$//' -e 's/[[:space:]]*$//') + + local IS_FALSE_POSITIVE=false + for FALSE_POSITIVE in "${KNOWN_FALSE_POSITIVES[@]}"; do + if [ "$VALUE" = "$FALSE_POSITIVE" ]; then + IS_FALSE_POSITIVE=true + break + fi + done + + if [ -z "$VALUE" ] || [ "$IS_FALSE_POSITIVE" = true ]; then + continue + fi + + ENV_SECRET_KEYS+=("$KEY") + ENV_SECRET_VALUES+=("$VALUE") + done < "$ENV_SECRETS_FILE" +} + +does_file_contain_dot_env_secret() { + local STAGED_FILE="$1" + local DISPLAY_PATH="$2" + local MATCHES="" + local INDEX=0 + local TOTAL=${#ENV_SECRET_VALUES[@]} + + while [ $INDEX -lt $TOTAL ]; do + local VALUE="${ENV_SECRET_VALUES[$INDEX]}" + local KEY="${ENV_SECRET_KEYS[$INDEX]}" + + while IFS= read -r LINE || [ -n "$LINE" ]; do + local LINE_NUMBER=${LINE%%:*} + + if is_known_false_positive_match "$STAGED_FILE" "$LINE_NUMBER"; then + continue + fi + + MATCHES+="[$DISPLAY_PATH:$LINE_NUMBER] Secret from .env file found (key: $KEY)\n" + done < <(grep -F -n -- "$VALUE" "$STAGED_FILE" || true) + + INDEX=$((INDEX + 1)) + done + + printf "%b" "$MATCHES" +} + +does_file_contain_potential_private_key() { + local STAGED_FILE="$1" + local DISPLAY_PATH="$2" + local MATCHES="" + + while IFS= read -r LINE || [ -n "$LINE" ]; do + if [ -z "$LINE" ]; then + continue + fi + + local LINE_NUMBER=${LINE%%:*} + local LINE_CONTENT=${LINE#*:} + + if is_known_false_positive_match "$STAGED_FILE" "$LINE_NUMBER"; then + continue + fi + + MATCHES+="Potential private key found:\n$DISPLAY_PATH:$LINE_NUMBER:$LINE_CONTENT\n\n" + done < <(grep -E -n -- "$ETH_PRIVATE_KEY_PATTERN" "$STAGED_FILE" || true) + + printf "%b" "$MATCHES" +} + +run_secret_checks() { + SECRET_FOUND=0 + PRIVATE_KEY_FOUND=0 + SECRET_RESULTS="" + + while IFS= read -r -d '' FILE; do + if is_excluded_path "$FILE"; then + continue + fi + + local STAGED_COPY + STAGED_COPY=$(mktemp "$TEMP_DIR/staged.XXXXXX") + + if ! git show ":$FILE" > "$STAGED_COPY" 2>/dev/null; then + rm -f "$STAGED_COPY" + continue + fi + + local RESULT_SECRET + RESULT_SECRET=$(does_file_contain_dot_env_secret "$STAGED_COPY" "$FILE") + if [ -n "$RESULT_SECRET" ]; then + SECRET_RESULTS+="$RESULT_SECRET\n" + SECRET_FOUND=1 + fi + + local RESULT_PRIVKEY + RESULT_PRIVKEY=$(does_file_contain_potential_private_key "$STAGED_COPY" "$FILE") + if [ -n "$RESULT_PRIVKEY" ]; then + SECRET_RESULTS+="$RESULT_PRIVKEY\n" + PRIVATE_KEY_FOUND=1 + fi + + rm -f "$STAGED_COPY" + done < <(git diff --cached --name-only --diff-filter=ACM -z) +} + +prepare_env_secrets # Start forge build and lint-staged in parallel (they're independent initially) FORGE_PID="" @@ -309,7 +324,6 @@ fi if [ -n "$FORGE_PID" ]; then if ! wait_and_check "$FORGE_PID" "$FORGE_EXIT" "$FORGE_OUTPUT" "Forge build failed. Aborting commit." "forge"; then [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null - wait "$SECRET_CHECK_PID" 2>/dev/null exit 1 fi print_status "success" "Forge build completed" @@ -323,7 +337,6 @@ if [ "$HAS_SOL_FILES" = "yes" ]; then if ! wait_and_check "$TYPECHAIN_PID" "$TYPECHAIN_EXIT" "$TYPECHAIN_OUTPUT" "TypeChain generation failed. Aborting commit." "typechain"; then [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null - wait "$SECRET_CHECK_PID" 2>/dev/null exit 1 fi # Extract and show summary from typechain output (filter verbose lines) @@ -339,7 +352,6 @@ if [ "$HAS_TS_JS_FILES" = "yes" ] && [ "$HAS_SOL_FILES" = "yes" ]; then if ! wait_and_check "$TSC_PID" "$TSC_EXIT" "$TSC_OUTPUT" "" "tsc"; then [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null - wait "$SECRET_CHECK_PID" 2>/dev/null printf '\n\033[31mTypeScript compilation failed.\033[0m\n' printf 'This may indicate outdated TypeChain types.\n' printf 'Please run \033[1mbun typechain\033[0m manually and fix any type errors before committing.\n\n' @@ -351,38 +363,36 @@ fi # Wait for lint-staged if it was started if [ -n "$LINT_STAGED_PID" ]; then if ! wait_and_check "$LINT_STAGED_PID" "$LINT_STAGED_EXIT" "$LINT_STAGED_OUTPUT" "Lint-staged failed. Aborting commit." "lint-staged"; then - wait "$SECRET_CHECK_PID" 2>/dev/null exit 1 fi print_status "success" "Lint-staged completed" fi -# Wait for secret checking and get results print_status "info" "Checking for secrets and private keys..." -wait "$SECRET_CHECK_PID" 2>/dev/null -SECRET_CHECK_EXIT_CODE=$(cat "$SECRET_CHECK_EXIT" 2>/dev/null || echo "1") -SECRET_RESULT=$(cat "$SECRET_CHECK_OUTPUT" 2>/dev/null || true) - -if [ "$SECRET_CHECK_EXIT_CODE" -ne 0 ]; then - # Show errors if any - if [ -n "$SECRET_RESULT" ]; then - echo "$SECRET_RESULT" - fi - exit 1 +run_secret_checks + +if [ -n "$SECRET_RESULTS" ]; then + echo "" + echo "-------------------------------------- RESULTS: ---------------------------------------------––" + printf '%b' "$SECRET_RESULTS" + echo "---------------------------------------------------------------------------------------------––" fi -# Show secret check results if any issues found -if echo "$SECRET_RESULT" | grep -q "Secret from .env file found"; then - printf '\n\033[36m⚠ Warning: Secret value(s) from .env found. This code cannot be committed.\033[0m\n' - printf '\033[91mRemove the secrets and try to commit again\033[0m\n\n' - # Show the specific matches - echo "$SECRET_RESULT" | grep "Secret from .env file found" || true +if [ "$SECRET_FOUND" -eq 1 ]; then + print_status "error" "Secret value(s) from .env found. This code cannot be committed." + print_status "error" "Remove the secrets and try to commit again" + echo "" exit 1 -elif echo "$SECRET_RESULT" | grep -q "Potential private key found"; then - printf '\n\033[33m⚠ Warning: Potential Ethereum private keys found\033[0m\n' - printf 'Check each match carefully before pushing to GitHub\n\n' - # Show the matches (limit to first few to avoid spam) - echo "$SECRET_RESULT" | grep -A 1 "Potential private key found" | head -10 || true +fi + +if [ "$PRIVATE_KEY_FOUND" -eq 1 ]; then + print_status "warning" "Potential Ethereum private keys found" + echo "" + print_status "warning" "NEXT STEPS" + print_status "warning" "Check each match carefully and make sure that no sensitive information is being committed" + print_status "warning" "If it did happen, undo the commit with 'git reset --soft HEAD~1', remove the secret(s) and commit again." + print_status "error" "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! BEFORE PUSHING TO GITHUB !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + echo "" print_status "success" "Secret check completed (warnings only)" else print_status "success" "Secret check passed" From a93d4e638cc9014cae915fd0319c0404b658f876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Fri, 12 Dec 2025 15:17:26 +0700 Subject: [PATCH 4/6] remove some false positives --- script/deploy/healthCheck.ts | 2 +- script/playgroundHelpers.sh | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/script/deploy/healthCheck.ts b/script/deploy/healthCheck.ts index 4689529d6..07c402649 100644 --- a/script/deploy/healthCheck.ts +++ b/script/deploy/healthCheck.ts @@ -1087,7 +1087,7 @@ const checkWhitelistIntegrity = async ( // cast keccak "com.lifi.library.allow.list" // 0x7a8ac5d3b7183f220a0602439da45ea337311d699902d1ed11a3725a714e7f1e const ALLOW_LIST_NAMESPACE = - '0x7a8ac5d3b7183f220a0602439da45ea337311d699902d1ed11a3725a714e7f1e' + '0x7a8ac5d3b7183f220a0602439da45ea337311d699902d1ed11a3725a714e7f1e' //[pre-commit-checker: not a secret] const baseSlot = BigInt(ALLOW_LIST_NAMESPACE) const contractAllowListSlot = baseSlot + 0n const selectorAllowListSlot = baseSlot + 1n diff --git a/script/playgroundHelpers.sh b/script/playgroundHelpers.sh index 1b013792d..c23807372 100644 --- a/script/playgroundHelpers.sh +++ b/script/playgroundHelpers.sh @@ -718,22 +718,20 @@ function analyzeFailingTx() { # Returns: # 0 on success, 1 on failure # Example: - # analyzeFailingTx "0xedc3d7580e0b333f7c232649b0506aa3e811b0f5060d84e75a91b0dec68b4cc9" "" + # analyzeFailingTx "" "" local TX_HASH="$1" local RPC_URL="$2" # Validate required parameters if [[ -z "$TX_HASH" ]]; then - error "Usage: analyzeFailingTx TX_HASH RPC_URL" - error "Example: analyzeFailingTx 0xedc3d7580e0b333f7c232649b0506aa3e811b0f5060d84e75a91b0dec68b4cc9 " + error "Example: analyzeFailingTx " return 1 fi if [[ -z "$RPC_URL" ]]; then error "RPC_URL is required" - error "Usage: analyzeFailingTx TX_HASH RPC_URL" - error "Example: analyzeFailingTx 0xedc3d7580e0b333f7c232649b0506aa3e811b0f5060d84e75a91b0dec68b4cc9 " + error "Example: analyzeFailingTx " return 1 fi From 99f1c6c798b33d0bd5a2ca3abd5bc3f63e4b5984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Mon, 22 Dec 2025 09:50:26 +0700 Subject: [PATCH 5/6] fix some issues from coderabbit and peer review --- .husky/pre-commit | 105 +++++++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 44 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 36594f920..2feaadc16 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,7 +3,7 @@ export HUSKY_SHELL="${HUSKY_SHELL:-bash}" . "$(dirname -- "$0")/_/husky.sh" # Early exit: Check if there are any staged files -STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM) +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR) if [ -z "$STAGED_FILES" ]; then echo "No files staged for commit. Skipping pre-commit checks." exit 0 @@ -15,13 +15,17 @@ cd "$GIT_ROOT" || exit 1 # Determine what types of files changed for conditional execution (optimized single pass) HAS_SOL_FILES="no" HAS_TS_JS_FILES="no" -HAS_TYPE_FILES="no" -echo "$STAGED_FILES" | grep -qE '\.sol$' && HAS_SOL_FILES="yes" && HAS_TYPE_FILES="yes" -echo "$STAGED_FILES" | grep -qE '\.(ts|js|tsx)$' && HAS_TS_JS_FILES="yes" && HAS_TYPE_FILES="yes" +# Check staged files for Solidity and TypeScript/JavaScript extensions +echo "$STAGED_FILES" | grep -qE '\.sol$' && HAS_SOL_FILES="yes" +echo "$STAGED_FILES" | grep -qE '\.(ts|js|tsx)$' && HAS_TS_JS_FILES="yes" # Create temp directory for parallel execution -TEMP_DIR=$(mktemp -d) +# Try portable form first, fall back to macOS-specific, then mkdir fallback +TEMP_DIR=$(mktemp -d 2>/dev/null) || TEMP_DIR=$(mktemp -d -t precommit 2>/dev/null) || { + TEMP_DIR="${TMPDIR:-/tmp}/precommit.$$.$(date +%s).$RANDOM" + mkdir -p "$TEMP_DIR" +} trap "rm -rf '$TEMP_DIR' 2>/dev/null" EXIT INT TERM FORGE_OUTPUT="$TEMP_DIR/forge.out" @@ -290,14 +294,46 @@ run_secret_checks() { fi rm -f "$STAGED_COPY" - done < <(git diff --cached --name-only --diff-filter=ACM -z) + done < <(git diff --cached --name-only --diff-filter=ACMR -z) } prepare_env_secrets +# Run secret checks early before heavy work +print_status "info" "Checking for secrets and private keys..." +run_secret_checks + +if [ -n "$SECRET_RESULTS" ]; then + echo "" + echo "-------------------------------------- RESULTS: ---------------------------------------------––" + printf '%b' "$SECRET_RESULTS" + echo "---------------------------------------------------------------------------------------------––" +fi + +if [ "$SECRET_FOUND" -eq 1 ]; then + print_status "error" "Secret value(s) from .env found. This code cannot be committed." + print_status "error" "Remove the secrets and try to commit again" + echo "" + exit 1 +fi + +if [ "$PRIVATE_KEY_FOUND" -eq 1 ]; then + print_status "warning" "Potential Ethereum private keys found" + echo "" + print_status "warning" "NEXT STEPS" + print_status "warning" "Check each match carefully and make sure that no sensitive information is being committed" + print_status "warning" "If it did happen, undo the commit with 'git reset --soft HEAD~1', remove the secret(s) and commit again." + print_status "error" "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! BEFORE PUSHING TO GITHUB !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + echo "" + print_status "success" "Secret check completed (warnings only)" +else + print_status "success" "Secret check passed" +fi + # Start forge build and lint-staged in parallel (they're independent initially) FORGE_PID="" LINT_STAGED_PID="" +TSC_PID="" print_section "Pre-commit Checks" @@ -311,19 +347,23 @@ else echo 0 > "$FORGE_EXIT" fi -if [ "$HAS_TYPE_FILES" = "yes" ]; then - print_status "info" "Running lint-staged (formatting & linting)..." - (bun lint-staged > "$LINT_STAGED_OUTPUT" 2>&1; echo $? > "$LINT_STAGED_EXIT") & - LINT_STAGED_PID=$! -else - print_status "skip" "Skipping lint-staged (no lintable files changed)" - echo 0 > "$LINT_STAGED_EXIT" +print_status "info" "Running lint-staged (formatting & linting)..." +(bun lint-staged > "$LINT_STAGED_OUTPUT" 2>&1; echo $? > "$LINT_STAGED_EXIT") & +LINT_STAGED_PID=$! + +# If we have TS/JS files but no Solidity files, start TypeScript compilation in parallel +# (it doesn't need to wait for typechain in this case) +if [ "$HAS_TS_JS_FILES" = "yes" ] && [ "$HAS_SOL_FILES" = "no" ]; then + print_status "info" "Checking TypeScript compilation..." + (bunx tsc-files --noEmit > "$TSC_OUTPUT" 2>&1; echo $? > "$TSC_EXIT") & + TSC_PID=$! fi # Wait for forge build if it was started if [ -n "$FORGE_PID" ]; then if ! wait_and_check "$FORGE_PID" "$FORGE_EXIT" "$FORGE_OUTPUT" "Forge build failed. Aborting commit." "forge"; then [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null + [ -n "$TSC_PID" ] && wait "$TSC_PID" 2>/dev/null exit 1 fi print_status "success" "Forge build completed" @@ -337,6 +377,7 @@ if [ "$HAS_SOL_FILES" = "yes" ]; then if ! wait_and_check "$TYPECHAIN_PID" "$TYPECHAIN_EXIT" "$TYPECHAIN_OUTPUT" "TypeChain generation failed. Aborting commit." "typechain"; then [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null + [ -n "$TSC_PID" ] && wait "$TSC_PID" 2>/dev/null exit 1 fi # Extract and show summary from typechain output (filter verbose lines) @@ -344,12 +385,18 @@ if [ "$HAS_SOL_FILES" = "yes" ]; then print_status "success" "$TYPECHAIN_SUMMARY" fi -# TypeScript compilation check (only if both TS/JS files changed AND typechain was regenerated) +# TypeScript compilation check (only if TS/JS files changed) +# If we have Solidity files, we need to wait for typechain (already done above) +# If we don't have Solidity files, tsc is already running in parallel if [ "$HAS_TS_JS_FILES" = "yes" ] && [ "$HAS_SOL_FILES" = "yes" ]; then print_status "info" "Checking TypeScript compilation..." (bunx tsc-files --noEmit > "$TSC_OUTPUT" 2>&1; echo $? > "$TSC_EXIT") & TSC_PID=$! +fi +# Wait for all remaining parallel tasks (tsc and lint-staged) +# Check if tsc is running and wait for it +if [ -n "$TSC_PID" ]; then if ! wait_and_check "$TSC_PID" "$TSC_EXIT" "$TSC_OUTPUT" "" "tsc"; then [ -n "$LINT_STAGED_PID" ] && wait "$LINT_STAGED_PID" 2>/dev/null printf '\n\033[31mTypeScript compilation failed.\033[0m\n' @@ -368,35 +415,5 @@ if [ -n "$LINT_STAGED_PID" ]; then print_status "success" "Lint-staged completed" fi -print_status "info" "Checking for secrets and private keys..." -run_secret_checks - -if [ -n "$SECRET_RESULTS" ]; then - echo "" - echo "-------------------------------------- RESULTS: ---------------------------------------------––" - printf '%b' "$SECRET_RESULTS" - echo "---------------------------------------------------------------------------------------------––" -fi - -if [ "$SECRET_FOUND" -eq 1 ]; then - print_status "error" "Secret value(s) from .env found. This code cannot be committed." - print_status "error" "Remove the secrets and try to commit again" - echo "" - exit 1 -fi - -if [ "$PRIVATE_KEY_FOUND" -eq 1 ]; then - print_status "warning" "Potential Ethereum private keys found" - echo "" - print_status "warning" "NEXT STEPS" - print_status "warning" "Check each match carefully and make sure that no sensitive information is being committed" - print_status "warning" "If it did happen, undo the commit with 'git reset --soft HEAD~1', remove the secret(s) and commit again." - print_status "error" "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! BEFORE PUSHING TO GITHUB !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" - echo "" - print_status "success" "Secret check completed (warnings only)" -else - print_status "success" "Secret check passed" -fi - printf '\n\033[1m━━━ All pre-commit checks passed! ━━━\033[0m\n' exit 0 From 629f0958996438bcca88aaa8457b9eaee0324973 Mon Sep 17 00:00:00 2001 From: Daniel <77058885+0xDEnYO@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:06:45 +0700 Subject: [PATCH 6/6] Update script/playgroundHelpers.sh Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- script/playgroundHelpers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/playgroundHelpers.sh b/script/playgroundHelpers.sh index b5464a7fd..de736cb64 100644 --- a/script/playgroundHelpers.sh +++ b/script/playgroundHelpers.sh @@ -719,7 +719,7 @@ function analyzeFailingTx() { # Returns: # 0 on success, 1 on failure # Example: - # analyzeFailingTx "" "" + # analyzeFailingTx "" "" local NETWORK="$1" local TX_HASH="$2"