#!/usr/bin/env bash set -u -o pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPORT_DIR="$ROOT_DIR/reports" ERROR_REPORT="$REPORT_DIR/errors.md" GENERATED_REPORT="$REPORT_DIR/generated-files.md" STATUS_FILE="$REPORT_DIR/customer-code-status.txt" TMP_DIR="" FILES_CHECKED=0 WARNINGS=0 ERRORS=0 DETAILS=() GENERATED_FILES=() EXCLUDES=( ".git" ".forgejo" "node_modules" "vendor" "Library" "Temp" "Logs" "obj" "bin" ) cleanup() { if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then rm -rf "$TMP_DIR" fi } trap cleanup EXIT record_generated_file() { local file="$1" local rel="${file#"$ROOT_DIR"/}" local existing for existing in "${GENERATED_FILES[@]}"; do if [[ "$existing" == "$rel" ]]; then return fi done GENERATED_FILES+=("$rel") } escape_md() { printf '%s' "$1" | sed 's/|/\\|/g' } have_tool() { command -v "$1" >/dev/null 2>&1 } ensure_report_dir() { mkdir -p "$REPORT_DIR" || { printf 'Failed to create report directory: %s\n' "$REPORT_DIR" >&2 return 1 } } add_warning() { local message="$1" WARNINGS=$((WARNINGS + 1)) DETAILS+=("- Warning: $(escape_md "$message")") } add_error() { local message="$1" ERRORS=$((ERRORS + 1)) DETAILS+=("- Error: $(escape_md "$message")") } add_ok() { local message="$1" DETAILS+=("- OK: $(escape_md "$message")") } relative_path() { local path="$1" path="${path#"$ROOT_DIR"/}" printf '%s' "$path" } join_by() { local delimiter="$1" shift || true local first=1 local item for item in "$@"; do [[ -z "$item" ]] && continue if [[ $first -eq 1 ]]; then printf '%s' "$item" first=0 else printf '%s%s' "$delimiter" "$item" fi done } collect_files() { local find_args=("$ROOT_DIR") local exclude if ((${#EXCLUDES[@]} > 0)); then find_args+=("(") for exclude in "${EXCLUDES[@]}"; do find_args+=(-name "$exclude" -o) done unset 'find_args[${#find_args[@]}-1]' find_args+=(")" -prune -o) fi find_args+=(-type f -print0) find "${find_args[@]}" } run_check() { local label="$1" local file="$2" shift 2 local stdout_file="$TMP_DIR/stdout.log" local stderr_file="$TMP_DIR/stderr.log" local stdout="" local stderr="" local combined="" FILES_CHECKED=$((FILES_CHECKED + 1)) : >"$stdout_file" : >"$stderr_file" if "$@" >"$stdout_file" 2>"$stderr_file"; then add_ok "$label passed: $(relative_path "$file")" else stdout="$(tr '\n' ' ' <"$stdout_file" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')" stderr="$(tr '\n' ' ' <"$stderr_file" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')" combined="$(join_by ' | ' "${stdout:-}" "${stderr:-}")" add_error "$label failed: $(relative_path "$file")${combined:+ | $combined}" fi } run_repo_build_checks() { local sln_files=() local csproj_files=() local cmake_file="$ROOT_DIR/CMakeLists.txt" local tmp_cmake_dir="" while IFS= read -r -d '' file; do sln_files+=("$file") done < <(find "$ROOT_DIR" -path "$ROOT_DIR/.git" -prune -o -path "$ROOT_DIR/.forgejo" -prune -o -name '*.sln' -type f -print0) while IFS= read -r -d '' file; do csproj_files+=("$file") done < <(find "$ROOT_DIR" -path "$ROOT_DIR/.git" -prune -o -path "$ROOT_DIR/.forgejo" -prune -o -name '*.csproj' -type f -print0) if ((${#sln_files[@]} > 0 || ${#csproj_files[@]} > 0)); then if have_tool dotnet; then if ((${#sln_files[@]} > 0)); then local file for file in "${sln_files[@]}"; do run_check "dotnet build" "$file" dotnet build "$file" --nologo --verbosity minimal done else local file for file in "${csproj_files[@]}"; do run_check "dotnet build" "$file" dotnet build "$file" --nologo --verbosity minimal done fi else add_warning "dotnet not installed; skipping C# project validation." fi fi if [[ -f "$cmake_file" ]]; then if have_tool cmake; then FILES_CHECKED=$((FILES_CHECKED + 1)) tmp_cmake_dir="$(mktemp -d)" if cmake -S "$ROOT_DIR" -B "$tmp_cmake_dir" >"$TMP_DIR/stdout.log" 2>"$TMP_DIR/stderr.log"; then add_ok "cmake configure passed: CMakeLists.txt" else local stdout stderr combined stdout="$(tr '\n' ' ' <"$TMP_DIR/stdout.log" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')" stderr="$(tr '\n' ' ' <"$TMP_DIR/stderr.log" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')" combined="$(join_by ' | ' "${stdout:-}" "${stderr:-}")" add_error "cmake configure failed: CMakeLists.txt${combined:+ | $combined}" fi rm -rf "$tmp_cmake_dir" else add_warning "cmake not installed; skipping C/C++ validation." fi fi } validate_file() { local file="$1" local base base="$(basename "$file")" case "$file" in *.php) if have_tool php; then run_check "php -l" "$file" php -l "$file" else add_warning "php not installed; skipping $(relative_path "$file")" fi ;; *.py) if have_tool python3; then run_check "python3 -m py_compile" "$file" python3 -m py_compile "$file" else add_warning "python3 not installed; skipping $(relative_path "$file")" fi ;; *.rb) if have_tool ruby; then run_check "ruby -c" "$file" ruby -c "$file" else add_warning "ruby not installed; skipping $(relative_path "$file")" fi ;; *.pl|*.pm) if have_tool perl; then run_check "perl -c" "$file" perl -c "$file" else add_warning "perl not installed; skipping $(relative_path "$file")" fi ;; *.sh|*.bash) if have_tool bash; then run_check "bash -n" "$file" bash -n "$file" else add_warning "bash not installed; skipping $(relative_path "$file")" fi ;; *.js|*.mjs|*.cjs) if have_tool node; then run_check "node --check" "$file" node --check "$file" else add_warning "node not installed; skipping $(relative_path "$file")" fi ;; *.json) if have_tool jq; then run_check "jq validation" "$file" jq empty "$file" else add_warning "jq not installed; skipping $(relative_path "$file")" fi ;; *.yml|*.yaml) if have_tool yamllint; then run_check "yamllint" "$file" yamllint -f parsable "$file" else add_warning "yamllint not installed; skipping $(relative_path "$file")" fi ;; *.html|*.htm) if have_tool tidy; then run_check "tidy validation" "$file" tidy -qe "$file" else add_warning "tidy not installed; skipping $(relative_path "$file")" fi ;; *) if [[ "$base" == "Dockerfile" ]]; then FILES_CHECKED=$((FILES_CHECKED + 1)) add_ok "Detected Dockerfile: $(relative_path "$file")" fi ;; esac } write_generated_files_report() { { printf '# Generated Files\n\n' if ((${#GENERATED_FILES[@]} == 0)); then printf 'No report files were generated.\n' else local file for file in "${GENERATED_FILES[@]}"; do printf -- '- %s\n' "$file" done fi } >"$GENERATED_REPORT" || return 1 record_generated_file "$GENERATED_REPORT" } write_status_file() { local status_line="NO CUSTOMER CODE ERRORS FOUND" if ((ERRORS > 0)); then status_line="CUSTOMER CODE ERRORS FOUND" fi printf '%s\n' "$status_line" >"$STATUS_FILE" || return 1 record_generated_file "$STATUS_FILE" } write_error_report() { local status_line="NO CUSTOMER CODE ERRORS FOUND" if ((ERRORS > 0)); then status_line="CUSTOMER CODE ERRORS FOUND" fi { printf '# Validation Summary\n\n' printf '%s\n\n' "$status_line" printf -- '- Files Checked: %s\n' "$FILES_CHECKED" printf -- '- Warnings: %s\n' "$WARNINGS" printf -- '- Errors: %s\n\n' "$ERRORS" printf '## Detailed Results\n\n' if ((${#DETAILS[@]} == 0)); then printf 'No validation steps were run.\n' else printf '%s\n' "${DETAILS[@]}" fi } >"$ERROR_REPORT" || return 1 record_generated_file "$ERROR_REPORT" } print_terminal_summary() { local status_line="NO CUSTOMER CODE ERRORS FOUND" if ((ERRORS > 0)); then status_line="CUSTOMER CODE ERRORS FOUND" fi printf '===== VALIDATION SUMMARY =====\n' printf '%s\n' "$status_line" printf 'Files checked: %s\n' "$FILES_CHECKED" printf 'Warnings: %s\n' "$WARNINGS" printf 'Errors: %s\n' "$ERRORS" printf 'Reports written:\n' local file for file in "${GENERATED_FILES[@]}"; do printf -- '- %s\n' "$file" done } main() { ensure_report_dir || return 1 TMP_DIR="$(mktemp -d)" || { printf 'Failed to create temporary directory.\n' >&2 return 1 } while IFS= read -r -d '' file; do validate_file "$file" done < <(collect_files) run_repo_build_checks write_status_file || return 1 write_error_report || return 1 write_generated_files_report || return 1 print_terminal_summary return 0 } main "$@"