diff --git a/.github/workflows/bats_tests.yml b/.github/workflows/bats_tests.yml new file mode 100644 index 0000000..ad92e10 --- /dev/null +++ b/.github/workflows/bats_tests.yml @@ -0,0 +1,104 @@ +name: Bats Tests + +on: + pull_request: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-bats-tests + cancel-in-progress: true + +jobs: + actionlint: + name: Validate GitHub Workflows (actionlint) + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install actionlint + run: | + ACTIONLINT_VERSION="1.7.7" + curl -sSfL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \ + | tar -xz + sudo mv actionlint /usr/local/bin/actionlint + actionlint -version + + - name: Run Workflow Linter + run: actionlint + + shellcheck: + name: Lint Shell Scripts (shellcheck) + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install shellcheck + run: | + sudo apt-get update -y + sudo apt-get install -y shellcheck + + - name: Run Shell Linter + run: shellcheck scripts/*.sh + + shfmt: + name: Check Shell Formatting (shfmt) + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install shfmt + run: | + sudo apt-get update -y + sudo apt-get install -y shfmt + + - name: Run Shell Formatter Check + run: git ls-files '*.sh' '*.bash' '*.bats' | xargs shfmt -d -i 4 -ci + + bats: + name: Run Shell Test Suite (bats) + needs: + - shellcheck + - shfmt + - actionlint + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install bats (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update -y + sudo apt-get install -y bats + + - name: Install bats (macOS) + if: runner.os == 'macOS' + run: brew install bats-core + + - name: Run bats Test Suite + run: bats --recursive tests/bats diff --git a/Makefile b/Makefile index 47b79c9..beb0b09 100644 --- a/Makefile +++ b/Makefile @@ -66,4 +66,16 @@ format: package: curl -s $(baseUrl)/check-swift-package.sh | bash +lint-shell: + curl -s $(baseUrl)/script-format.sh | bash + +format-shell: + curl -s $(baseUrl)/script-format.sh | bash -s -- --fix + +lint-workflows: + curl -s $(baseUrl)/run-actionlint.sh | bash + check: symlinks language deps lint docc-warnings headers + +test-bats: + bats --recursive tests/bats diff --git a/README.md b/README.md index d38ec73..8d57ea8 100644 --- a/README.md +++ b/README.md @@ -146,8 +146,8 @@ Detects source- and binary-level API breaking changes in Swift packages to preve #### Behavior * Uses `swift package diagnose-api-breaking-changes` -* Pull requests: compares against the PR base branch -* Other contexts: compares against the latest Git tag +* Pull requests: fetches `${GITHUB_BASE_REF}` into a local `pull-base-ref` and compares against that ref +* Other contexts: fetches tags and compares against the latest Git tag * If no tags exist, exits successfully with a warning * Fails when breaking changes are detected @@ -233,7 +233,7 @@ Prevents accidental usage of local Swift package dependencies. #### Behavior -* Scans all tracked `Package.swift` files +* Scans git-tracked `Package.swift` files only (ignores untracked files) * Detects `.package(path:)` * Fails immediately on detection @@ -262,22 +262,29 @@ Runs a security scan of an OpenAPI specification using OWASP ZAP. #### Behavior * Executes inside Docker -* Skips execution if `openapi/` directory does not exist +* Accepts an OpenAPI file or directory (default: `openapi`) +* Relative `-f` paths are resolved from the git repository root first, then current working directory +* For file paths, supports `.yml`/`.yaml` extension fallback +* Skips execution if no OpenAPI specification can be resolved #### Parameters -_None_ +* `-f ` – OpenAPI file or directory path #### Ignore files _None_ -#### Raw curl example +#### Raw curl examples ```sh curl -s $(baseUrl)/check-openapi-security.sh | bash ``` +```sh +curl -s $(baseUrl)/check-openapi-security.sh | bash -s -- -f openapi/openapi.yml +``` + --- ### check-openapi-validation.sh @@ -289,22 +296,34 @@ Validates an OpenAPI specification for schema correctness. #### Behavior * Runs the OpenAPI validator in Docker -* Skips execution if `openapi/` directory does not exist +* Accepts an OpenAPI file or directory (default: `openapi`) +* Relative `-f` paths are resolved from the git repository root first, then current working directory +* For file paths, supports `.yml`/`.yaml` extension fallback +* Skips execution if no OpenAPI specification can be resolved #### Parameters -_None_ +* `-f ` – OpenAPI file or directory path #### Ignore files _None_ -#### Raw curl example +#### Raw curl examples ```sh curl -s $(baseUrl)/check-openapi-validation.sh | bash ``` +```sh +curl -s $(baseUrl)/check-openapi-validation.sh | bash -s -- -f openapi/openapi.yaml +``` + +```sh +# Monorepo/nested project example (path relative to git root) +curl -s $(baseUrl)/check-openapi-validation.sh | bash -s -- -f mail-examples/mail-example-openapi/openapi/openapi.yaml +``` + --- ### check-swift-headers.sh @@ -318,6 +337,10 @@ Ensures Swift source files contain a consistent, standardized header. * Enforces a strict 5-line header format * Can optionally insert or update headers in-place * Processes only git-tracked Swift files +* Skips `Package.swift` explicitly +* Accepts `Created by ... on YYYY. MM. DD.` and legacy `..` suffix +* In `--fix` mode, normalizes legacy `..` to `.` +* When repairing malformed headers, preserves extracted author and date when possible #### Parameters @@ -326,7 +349,7 @@ Ensures Swift source files contain a consistent, standardized header. #### Ignore files -* `.swiftheaderignore` – excludes paths from header validation +* `.swiftheaderignore` – excludes paths from header validation (replaces default exclusions when present) #### Raw curl examples @@ -354,6 +377,7 @@ Detects discouraged or outdated terminology to promote inclusive language. * Case-insensitive, whole-word matching * Scans git-tracked files only +* Lines containing `ignore-unacceptable-language` are excluded from failures #### Parameters @@ -379,9 +403,11 @@ Generates a CONTRIBUTORS.txt file from git commit history. #### Behavior -* Uses `git shortlog` +* Uses `git shortlog -es HEAD` * Respects `.mailmap` * Overwrites the file deterministically +* Writes to repository root even when run from a subdirectory +* If the repository has no commits, exits successfully and does not create `CONTRIBUTORS.txt` #### Parameters @@ -506,7 +532,8 @@ Removes generated build artifacts and temporary files. ⚠️ Irreversible opera #### Behavior -* Deletes `.build`, `.swiftpm`, and generated files +* Deletes `.build/` and `.swiftpm/` +* Deletes `openapi/openapi.yaml`, `db.sqlite`, and `migration-entries.json` * Intended for local development use @@ -563,6 +590,10 @@ Serves OpenAPI documentation locally using Docker. #### Behavior +* Accepts an OpenAPI file or directory (default: `openapi`) +* Relative `-f` paths are resolved from the git repository root first, then current working directory +* If a file is provided, mounts its parent directory +* For file paths, supports `.yml`/`.yaml` extension fallback * Runs Nginx in the foreground * Exposes documentation over HTTP @@ -570,17 +601,22 @@ Serves OpenAPI documentation locally using Docker. * `-n ` – container name * `-p ` – port mapping +* `-f ` – OpenAPI file or directory path #### Ignore files _None_ -#### Raw curl example +#### Raw curl examples ```sh curl -s $(baseUrl)/run-openapi-docker.sh | bash -s -- -n openapi-preview ``` +```sh +curl -s $(baseUrl)/run-openapi-docker.sh | bash -s -- -n openapi-preview -f openapi/openapi.yaml +``` + --- ### run-swift-format.sh @@ -619,6 +655,71 @@ curl -s $(baseUrl)/run-swift-format.sh | bash -s -- --fix --- +### run-actionlint.sh + +#### Purpose + +Runs `actionlint` to validate GitHub Actions workflows. + +#### Behavior + +* Verifies `actionlint` is installed before running +* Runs from repository root for consistent workflow path resolution +* Passes through optional CLI arguments to `actionlint` + +#### Parameters + +* `` – optional arguments passed to `actionlint` + +#### Ignore files + +_None_ + +#### Raw curl example + +```sh +curl -s $(baseUrl)/run-actionlint.sh | bash +``` + +--- + +### script-format.sh + +#### Purpose + +Runs `shfmt` to check or fix formatting for tracked shell-related files. + +#### Behavior + +* Verifies `shfmt` is installed before running +* Targets tracked `*.sh`, `*.bash`, and `*.bats` files +* Default mode checks formatting and fails on drift +* `--fix` mode applies formatting in-place + +#### Parameters + +* `--fix` – apply formatting in-place + +#### Ignore files + +_None_ + +#### Raw curl examples + +_Check only:_ + +```sh +curl -s $(baseUrl)/script-format.sh | bash +``` + +_Fix formatting:_ + +```sh +curl -s $(baseUrl)/script-format.sh | bash -s -- --fix +``` + +--- + ### check-swift-package.sh #### Purpose diff --git a/scripts/check-api-breakage.sh b/scripts/check-api-breakage.sh index 67406fa..21bfcdc 100755 --- a/scripts/check-api-breakage.sh +++ b/scripts/check-api-breakage.sh @@ -13,9 +13,12 @@ set -euo pipefail # Logging helpers -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Determine baseline reference if [ -n "${GITHUB_BASE_REF:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_SERVER_URL:-}" ]; then @@ -45,4 +48,4 @@ log "Using baseline: ${BASELINE_REF}" # This command exits non-zero if API-breaking changes are detected. swift package diagnose-api-breaking-changes "$BASELINE_REF" -log "✅ No API-breaking changes detected." \ No newline at end of file +log "✅ No API-breaking changes detected." diff --git a/scripts/check-broken-symlinks.sh b/scripts/check-broken-symlinks.sh index b5bc0a6..ee03c10 100755 --- a/scripts/check-broken-symlinks.sh +++ b/scripts/check-broken-symlinks.sh @@ -17,9 +17,12 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent CI and local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Resolve the repository root # Ensures correct path resolution even if the script is run from a subdirectory @@ -51,4 +54,4 @@ if [ "${NUM_BROKEN_SYMLINKS}" -gt 0 ]; then fi # Success -log "✅ Found 0 broken symlinks." \ No newline at end of file +log "✅ Found 0 broken symlinks." diff --git a/scripts/check-docc-warnings.sh b/scripts/check-docc-warnings.sh index 69cd32f..720c9d3 100755 --- a/scripts/check-docc-warnings.sh +++ b/scripts/check-docc-warnings.sh @@ -22,22 +22,24 @@ set -euo pipefail # Logging helpers # # All logs go to stderr to keep stdout clean and CI-friendly. -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Configuration / state -TARGETS_FILE=".docctargetlist" # Optional file defining an explicit list of DocC targets -TARGETS="" # Raw newline-separated target names (internal representation) -TARGET_LIST=() # Parsed target names as a Bash array for iteration +TARGETS_FILE=".docctargetlist" # Optional file defining an explicit list of DocC targets +TARGETS="" # Raw newline-separated target names (internal representation) +TARGET_LIST=() # Parsed target names as a Bash array for iteration # Swift package manifest to mutate PACKAGE_FILE="Package.swift" # Required injection anchor inside Package.dependencies -INJECT_MARKER='// [docc-plugin-placeholder]' +INJECT_MARKER='// [docc-plugin-placeholder]' # Dependency line injected immediately after the marker DOCC_DEP=' .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.0"),' - # Git safety (local runs only) # @@ -120,13 +122,13 @@ ensure_docc_plugin() { END { if (!injected) exit 42 } - ' "$PACKAGE_FILE" > "$PACKAGE_FILE.tmp" + ' "$PACKAGE_FILE" >"$PACKAGE_FILE.tmp" mv "$PACKAGE_FILE.tmp" "$PACKAGE_FILE" # Validate manifest after mutation - swift package dump-package >/dev/null \ - || fatal "Package.swift became invalid after injecting swift-docc-plugin" + swift package dump-package >/dev/null || + fatal "Package.swift became invalid after injecting swift-docc-plugin" } # Pre-flight checks @@ -153,8 +155,8 @@ auto_detect_targets() { fatal "jq is required. Install with: brew install jq" fi - TARGETS=$(swift package dump-package \ - | jq -r '.targets[] + TARGETS=$(swift package dump-package | + jq -r '.targets[] | select(.type == "regular" or .type == "executable") | .name') @@ -177,7 +179,7 @@ fi # Convert newline-separated target list into an array while IFS= read -r TARGET; do TARGET_LIST+=("$TARGET") -done <<< "$TARGETS" +done <<<"$TARGETS" TARGET_COUNT="${#TARGET_LIST[@]}" log "Targets detected: $TARGET_COUNT" @@ -215,4 +217,4 @@ fi log "✅ All targets passed DocC analysis without warnings." -reset_git_after_analysis \ No newline at end of file +reset_git_after_analysis diff --git a/scripts/check-local-swift-dependencies.sh b/scripts/check-local-swift-dependencies.sh index 35e929e..131bc8a 100755 --- a/scripts/check-local-swift-dependencies.sh +++ b/scripts/check-local-swift-dependencies.sh @@ -21,9 +21,12 @@ set -euo pipefail # Logging helpers # # All output goes to stderr to keep stdout clean and CI-friendly. -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Determine repository root # @@ -39,10 +42,10 @@ REPO_ROOT="$(git -C "$PWD" rev-parse --show-toplevel)" # # Currently, we only check Package.swift, since local package references # are only valid in that file. -read -ra PATHS_TO_CHECK <<< "$( +read -ra PATHS_TO_CHECK <<<"$( git -C "${REPO_ROOT}" ls-files -z \ - "Package.swift" \ - | xargs -0 + "Package.swift" | + xargs -0 )" # Scan files for local Swift package references @@ -58,4 +61,4 @@ if [ -n "${PATHS_TO_CHECK+x}" ]; then fi # Success -log "✅ Found 0 local Swift package dependency references." \ No newline at end of file +log "✅ Found 0 local Swift package dependency references." diff --git a/scripts/check-openapi-security.sh b/scripts/check-openapi-security.sh index c0423ea..6f48f9f 100755 --- a/scripts/check-openapi-security.sh +++ b/scripts/check-openapi-security.sh @@ -14,33 +14,114 @@ set -euo pipefail # Logging helpers # All output goes to stderr for consistent CI logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} -# Resolve repository root -# This allows the script to be run from any subdirectory -REPO_ROOT="$(git -C "$PWD" rev-parse --show-toplevel)" +# Resolve script directory +# This allows the script to be run from any subdirectory. +SCRIPT_SOURCE="${BASH_SOURCE[0]-$0}" +SCRIPT_DIR="$(cd -- "$(dirname -- "${SCRIPT_SOURCE}")" && pwd)" -# Expected location of the OpenAPI specification -# The directory is expected to contain an `openapi.yaml` file -OPENAPI_YAML_LOCATION="${REPO_ROOT}/openapi" +OPENAPI_PATH="openapi" -# If no OpenAPI directory exists, skip the check gracefully -# This allows repositories without APIs to pass without failure -if [ ! -d "${OPENAPI_YAML_LOCATION}" ]; then - log "❗ OpenAPI location not found — skipping security scan." +resolve_repo_root() { + # Prefer git root for local execution and subdirectory calls. + if root="$(git rev-parse --show-toplevel 2>/dev/null)"; then + printf '%s\n' "${root}" + return 0 + fi + + # Fallback for checked-out scripts under a conventional ./scripts layout. + if [ -d "${SCRIPT_DIR}/../.git" ]; then + ( + cd -- "${SCRIPT_DIR}/.." + pwd + ) + return 0 + fi + + # Last resort for piped execution (for example: curl | bash). + pwd +} + +usage() { + cat >&2 <&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} -# Resolve the repository root -# Allows the script to be run from any subdirectory -REPO_ROOT="$(git -C "$PWD" rev-parse --show-toplevel)" +# Resolve script directory +# Allows the script to be run from any subdirectory. +SCRIPT_SOURCE="${BASH_SOURCE[0]-$0}" +SCRIPT_DIR="$(cd -- "$(dirname -- "${SCRIPT_SOURCE}")" && pwd)" -# Location of the OpenAPI specification directory -# The directory is expected to contain an `openapi.yaml` file -OPENAPI_YAML_LOCATION="${REPO_ROOT}/openapi" +OPENAPI_PATH="openapi" -# If the OpenAPI directory does not exist, skip validation gracefully -# This avoids failing CI for repositories that do not define APIs -if [ ! -d "${OPENAPI_YAML_LOCATION}" ]; then - log "❗ OpenAPI location not found — skipping validation." +resolve_repo_root() { + # Prefer git root for local execution and subdirectory calls. + if root="$(git rev-parse --show-toplevel 2>/dev/null)"; then + printf '%s\n' "${root}" + return 0 + fi + + # Fallback for checked-out scripts under a conventional ./scripts layout. + if [ -d "${SCRIPT_DIR}/../.git" ]; then + ( + cd -- "${SCRIPT_DIR}/.." + pwd + ) + return 0 + fi + + # Last resort for piped execution (for example: curl | bash). + pwd +} + +usage() { + cat >&2 <&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Configuration / state DEFAULT_AUTHOR="Binary Birds" @@ -25,66 +27,67 @@ HAS_ERRORS=0 # Argument parsing while [ $# -gt 0 ]; do - case "$1" in - --fix) - FIX_MODE=1 - ;; - --author) - shift - [ -z "${1:-}" ] && fatal "--author requires a value" - DEFAULT_AUTHOR="$1" - ;; - *) - fatal "Unknown argument: $1" - ;; - esac - shift + case "$1" in + --fix) + FIX_MODE=1 + ;; + --author) + shift + [ -z "${1:-}" ] && fatal "--author requires a value" + DEFAULT_AUTHOR="$1" + ;; + *) + fatal "Unknown argument: $1" + ;; + esac + shift done -[ "$FIX_MODE" -eq 1 ] \ - && log "Fix mode enabled — headers will be inserted or corrected." \ - || log "Checking Swift file headers..." - +if [ "$FIX_MODE" -eq 1 ]; then + log "Fix mode enabled — headers will be inserted or corrected." +else + log "Checking Swift file headers..." +fi # Project name PROJECT_NAME="$(basename "$PWD")" # Date helpers normalize_date() { - local raw="$1" - if command -v gdate >/dev/null 2>&1; then - gdate -d "$raw" +"%Y. %m. %d" 2>/dev/null - else - date -d "$raw" +"%Y. %m. %d" 2>/dev/null - fi + local raw="$1" + if command -v gdate >/dev/null 2>&1; then + gdate -d "$raw" +"%Y. %m. %d" 2>/dev/null + else + date -d "$raw" +"%Y. %m. %d" 2>/dev/null + fi } get_file_creation_date() { - local file="$1" - if git ls-files --error-unmatch "$file" >/dev/null 2>&1; then - git log -1 --format="%ad" --date=format:"%Y. %m. %d" -- "$file" - else - date +"%Y. %m. %d" - fi + local file="$1" + if git ls-files --error-unmatch "$file" >/dev/null 2>&1; then + git log -1 --format="%ad" --date=format:"%Y. %m. %d" -- "$file" + else + date +"%Y. %m. %d" + fi } # Header detection helpers # Find the line number of the "Created by" line (header anchor) find_header_line() { - grep -nE '^// Created by .+ on [0-9]{4}\. [0-9]{2}\. [0-9]{2}\.{1,2}$' "$1" \ - | head -n1 \ - | cut -d: -f1 + grep -nE '^// Created by .+ on [0-9]{4}\. [0-9]{2}\. [0-9]{2}\.{1,2}$' "$1" | + head -n1 | + cut -d: -f1 } # Validate header structure relative to the anchor line is_header_valid_at_line() { - local file="$1" - local line="$2" - local filename - filename=$(basename "$file") + local file="$1" + local line="$2" + local filename + filename=$(basename "$file") - sed -n "$((line-4)),${line}p" "$file" | awk -v f="$filename" -v p="$PROJECT_NAME" ' + sed -n "$((line - 4)),${line}p" "$file" | awk -v f="$filename" -v p="$PROJECT_NAME" ' NR==1 && $0=="//" {next} NR==2 && $0=="// " f {next} NR==3 && $0=="// " p {next} @@ -95,97 +98,110 @@ is_header_valid_at_line() { } extract_author_from_header_line() { - sed -E 's|^// Created by (.+) on [0-9]{4}\. [0-9]{2}\. [0-9]{2}\.{1,2}$|\1|' + sed -E 's|^// Created by (.+) on [0-9]{4}\. [0-9]{2}\. [0-9]{2}\.{1,2}$|\1|' } extract_date_from_header_line() { - sed -E 's|^// Created by .+ on ([0-9]{4}\. [0-9]{2}\. [0-9]{2})\.{1,2}$|\1|' + sed -E 's|^// Created by .+ on ([0-9]{4}\. [0-9]{2}\. [0-9]{2})\.{1,2}$|\1|' +} + +# Returns 0 when the header line uses legacy double-dot suffix. +is_legacy_double_dot_header_line() { + [[ "$1" =~ \.\.$ ]] } # Replace an invalid header block in place replace_header_at_line() { - local file="$1" - local line="$2" - local tmpfile - tmpfile=$(mktemp) - - # Extract original author and date - local header_line - header_line=$(sed -n "${line}p" "$file") - - local author - author=$(printf '%s\n' "$header_line" | extract_author_from_header_line) - - local date - date=$(printf '%s\n' "$header_line" | extract_date_from_header_line) - - # Safety fallback (should never happen) - [ -z "$author" ] && author="$DEFAULT_AUTHOR" - [ -z "$date" ] && date="$(get_file_creation_date "$file")" - - # Remove the old header block - local start=$((line-4)) - [ "$start" -lt 1 ] && start=1 - sed "${start},${line}d" "$file" > "$tmpfile" - - # Rebuild file WITHOUT adding extra blank lines - { - echo "//" - echo "// $(basename "$file")" - echo "// $PROJECT_NAME" - echo "//" - echo "// Created by $author on $date." - cat "$tmpfile" - } > "$tmpfile.fixed" - - mv "$tmpfile.fixed" "$file" -} + local file="$1" + local line="$2" + local tmpfile + tmpfile=$(mktemp) -# Header validation / fixing -check_or_fix_header() { - local file="$1" - local filename - filename=$(basename "$file") + # Extract original author and date + local header_line + header_line=$(sed -n "${line}p" "$file") - # Explicitly ignore Package.swift - [ "$filename" = "Package.swift" ] && return 0 + local author + author=$(printf '%s\n' "$header_line" | extract_author_from_header_line) - local header_line - header_line=$(find_header_line "$file" || true) + local date + date=$(printf '%s\n' "$header_line" | extract_date_from_header_line) - if [ -n "$header_line" ]; then - if is_header_valid_at_line "$file" "$header_line"; then - return 0 - fi + # Safety fallback (should never happen) + [ -z "$author" ] && author="$DEFAULT_AUTHOR" + [ -z "$date" ] && date="$(get_file_creation_date "$file")" - if [ "$FIX_MODE" -eq 1 ]; then - replace_header_at_line "$file" "$header_line" - log "Fixed header: $file" - else - error "❌ $file - Header is invalid" - return 1 - fi - else - # No header anywhere → insert at top - if [ "$FIX_MODE" -eq 1 ]; then - local tmpfile - tmpfile=$(mktemp) - { + # Remove the old header block + local start=$((line - 4)) + [ "$start" -lt 1 ] && start=1 + sed "${start},${line}d" "$file" >"$tmpfile" + + # Rebuild file WITHOUT adding extra blank lines + { echo "//" - echo "// $filename" + echo "// $(basename "$file")" echo "// $PROJECT_NAME" echo "//" - echo "// Created by $DEFAULT_AUTHOR on $(get_file_creation_date "$file")." - echo "" - cat "$file" - } > "$tmpfile" - mv "$tmpfile" "$file" - log "Header added: $file" + echo "// Created by $author on $date." + cat "$tmpfile" + } >"$tmpfile.fixed" + + mv "$tmpfile.fixed" "$file" +} + +# Header validation / fixing +check_or_fix_header() { + local file="$1" + local filename + filename=$(basename "$file") + + # Explicitly ignore Package.swift + [ "$filename" = "Package.swift" ] && return 0 + + local header_line + header_line=$(find_header_line "$file" || true) + + if [ -n "$header_line" ]; then + if is_header_valid_at_line "$file" "$header_line"; then + if [ "$FIX_MODE" -eq 1 ]; then + local header_text + header_text=$(sed -n "${header_line}p" "$file") + if is_legacy_double_dot_header_line "$header_text"; then + replace_header_at_line "$file" "$header_line" + log "Normalized legacy header: $file" + fi + fi + return 0 + fi + + if [ "$FIX_MODE" -eq 1 ]; then + replace_header_at_line "$file" "$header_line" + log "Fixed header: $file" + else + error "❌ $file - Header is invalid" + return 1 + fi else - error "❌ $file - Header missing" - return 1 + # No header anywhere → insert at top + if [ "$FIX_MODE" -eq 1 ]; then + local tmpfile + tmpfile=$(mktemp) + { + echo "//" + echo "// $filename" + echo "// $PROJECT_NAME" + echo "//" + echo "// Created by $DEFAULT_AUTHOR on $(get_file_creation_date "$file")." + echo "" + cat "$file" + } >"$tmpfile" + mv "$tmpfile" "$file" + log "Header added: $file" + else + error "❌ $file - Header missing" + return 1 + fi fi - fi } # Exclusions @@ -193,44 +209,52 @@ IGNORE_FILE=".swiftheaderignore" EXCLUDE_PATTERNS=() if [ -f "$IGNORE_FILE" ]; then - log "Using exclusion list from $IGNORE_FILE" - while IFS= read -r line || [ -n "$line" ]; do - [[ -n "$line" && ! "$line" =~ ^# ]] && EXCLUDE_PATTERNS+=(":(exclude)$line") - done < "$IGNORE_FILE" + log "Using exclusion list from $IGNORE_FILE" + while IFS= read -r line || [ -n "$line" ]; do + [[ -n "$line" && ! "$line" =~ ^# ]] && EXCLUDE_PATTERNS+=(":(exclude)$line") + done <"$IGNORE_FILE" else - EXCLUDE_PATTERNS+=( - ":(exclude).*" - ":(exclude)*.txt" - ":(exclude)*.png" - ":(exclude)*.jpeg" - ":(exclude)*.jpg" - ":(exclude)*.sh" - ":(exclude)*.html" - ":(exclude)*.yaml" - ":(exclude)README.md" - ":(exclude)Package.resolved" - ":(exclude)Makefile" - ":(exclude)LICENSE" - ":(exclude)Package*.swift" - ":(exclude)docker/**" - ) + EXCLUDE_PATTERNS+=( + ":(exclude).*" + ":(exclude)*.txt" + ":(exclude)*.png" + ":(exclude)*.jpeg" + ":(exclude)*.jpg" + ":(exclude)*.sh" + ":(exclude)*.html" + ":(exclude)*.yaml" + ":(exclude)README.md" + ":(exclude)Package.resolved" + ":(exclude)Makefile" + ":(exclude)LICENSE" + ":(exclude)Package*.swift" + ":(exclude)docker/**" + ) fi # File processing FILES=() while IFS= read -r -d '' file; do - FILES+=("$file") + FILES+=("$file") done < <(git ls-files -z "${EXCLUDE_PATTERNS[@]}") for file in "${FILES[@]}"; do - if ! check_or_fix_header "$file"; then - HAS_ERRORS=1 - fi + if ! check_or_fix_header "$file"; then + HAS_ERRORS=1 + fi done # Final result if [ "$HAS_ERRORS" -eq 1 ]; then - [ "$FIX_MODE" -eq 1 ] && log "⚠️ Some headers were fixed." || fatal "Some files have header issues." + if [ "$FIX_MODE" -eq 1 ]; then + log "⚠️ Some headers were fixed." + else + fatal "Some files have header issues." + fi else - [ "$FIX_MODE" -eq 1 ] && log "✅ Headers updated successfully." || log "✅ All headers are valid." -fi \ No newline at end of file + if [ "$FIX_MODE" -eq 1 ]; then + log "✅ Headers updated successfully." + else + log "✅ All headers are valid." + fi +fi diff --git a/scripts/check-swift-package.sh b/scripts/check-swift-package.sh index c398371..274d85a 100755 --- a/scripts/check-swift-package.sh +++ b/scripts/check-swift-package.sh @@ -6,19 +6,22 @@ set -u ERROR_COUNT=0 # Logging helpers -log() { printf -- "%s\n" "$*" >&2; } -error() { printf -- "%s\n" "$*" >&2; ERROR_COUNT=$((ERROR_COUNT + 1)); } +log() { printf -- "%s\n" "$*" >&2; } +error() { + printf -- "%s\n" "$*" >&2 + ERROR_COUNT=$((ERROR_COUNT + 1)) +} fatal() { - printf -- "%s\n" "$*" >&2 - exit 1 + printf -- "%s\n" "$*" >&2 + exit 1 } fatal_if_errors() { - if [ "$ERROR_COUNT" -gt 0 ]; then - printf -- "\n❌ %d error(s) found\n" "$ERROR_COUNT" >&2 - exit 1 - fi + if [ "$ERROR_COUNT" -gt 0 ]; then + printf -- "\n❌ %d error(s) found\n" "$ERROR_COUNT" >&2 + exit 1 + fi } log "Starting Swift Package validation" @@ -28,12 +31,12 @@ command -v jq >/dev/null 2>&1 || fatal "❌ jq not found (required)" # Helper to check required files check_file() { - file="$1" - if [ -f "$file" ]; then - log "✅ $file exists" - else - error "❌ $file missing (expected at repository root)" - fi + file="$1" + if [ -f "$file" ]; then + log "✅ $file exists" + else + error "❌ $file missing (expected at repository root)" + fi } # Package.swift @@ -42,65 +45,65 @@ check_file "Package.swift" # swift-tools-version TOOLS_LINE="$(grep '^// swift-tools-version:' Package.swift 2>/dev/null || true)" if [ -n "$TOOLS_LINE" ]; then - TOOLS_VERSION="$(printf "%s" "$TOOLS_LINE" | sed 's/.*: *//')" - major="$(printf "%s" "$TOOLS_VERSION" | cut -d. -f1)" - minor="$(printf "%s" "$TOOLS_VERSION" | cut -d. -f2)" - - if [ "$major" -lt 6 ] || { [ "$major" -eq 6 ] && [ "$minor" -lt 1 ]; }; then - error "❌ swift-tools-version too old ($TOOLS_VERSION), expected >= 6.1" - else - log "✅ swift-tools-version >= 6.1 ($TOOLS_VERSION)" - fi + TOOLS_VERSION="$(printf "%s" "$TOOLS_LINE" | sed 's/.*: *//')" + major="$(printf "%s" "$TOOLS_VERSION" | cut -d. -f1)" + minor="$(printf "%s" "$TOOLS_VERSION" | cut -d. -f2)" + + if [ "$major" -lt 6 ] || { [ "$major" -eq 6 ] && [ "$minor" -lt 1 ]; }; then + error "❌ swift-tools-version too old ($TOOLS_VERSION), expected >= 6.1" + else + log "✅ swift-tools-version >= 6.1 ($TOOLS_VERSION)" + fi else - error "❌ swift-tools-version missing" + error "❌ swift-tools-version missing" fi # defaultSwiftSettings if grep -q 'defaultSwiftSettings' Package.swift 2>/dev/null; then - log "✅ defaultSwiftSettings found" + log "✅ defaultSwiftSettings found" else - error "❌ defaultSwiftSettings missing" + error "❌ defaultSwiftSettings missing" fi # Swift 6 concurrency default (string presence only) if grep -q '"NonisolatedNonsendingByDefault"' Package.swift 2>/dev/null; then - log "✅ NonisolatedNonsendingByDefault present" + log "✅ NonisolatedNonsendingByDefault present" else - error "❌ NonisolatedNonsendingByDefault missing" + error "❌ NonisolatedNonsendingByDefault missing" fi # Parse Package.swift via SwiftPM PACKAGE_JSON="$(swift package dump-package 2>/dev/null || true)" if [ -n "$PACKAGE_JSON" ]; then - log "✅ Package.swift parsed by SwiftPM" + log "✅ Package.swift parsed by SwiftPM" else - error "❌ Failed to parse Package.swift via SwiftPM" + error "❌ Failed to parse Package.swift via SwiftPM" fi # Top-level dependencies if [ -n "$PACKAGE_JSON" ]; then - if echo "$PACKAGE_JSON" | jq -e '.dependencies | type == "array"' >/dev/null; then - log "✅ Top-level dependencies array exists" - else - error "❌ Top-level dependencies missing" - fi + if echo "$PACKAGE_JSON" | jq -e '.dependencies | type == "array"' >/dev/null; then + log "✅ Top-level dependencies array exists" + else + error "❌ Top-level dependencies missing" + fi fi # docc placeholder if grep -q '// *\[docc-plugin-placeholder\]' Package.swift 2>/dev/null; then - log "✅ docc plugin placeholder present" + log "✅ docc plugin placeholder present" else - error "❌ docc plugin placeholder missing" + error "❌ docc plugin placeholder missing" fi # swiftSettings: defaultSwiftSettings on all targets if [ -n "$PACKAGE_JSON" ]; then - TARGETS="$(echo "$PACKAGE_JSON" | jq -r '.targets[].name')" + TARGETS="$(echo "$PACKAGE_JSON" | jq -r '.targets[].name')" - for target in $TARGETS; do - if awk ' + for target in $TARGETS; do + if awk -v target="$target" ' # Start scanning when we see the target name - /name:[[:space:]]*"'$target'"/ { + $0 ~ "name:[[:space:]]*\"" target "\"" { in_target = 1 } @@ -122,26 +125,25 @@ if [ -n "$PACKAGE_JSON" ]; then END { exit !found } - ' Package.swift - then - log "✅ $target uses swiftSettings: defaultSwiftSettings" - else - error "❌ $target missing swiftSettings: defaultSwiftSettings" - fi - done + ' Package.swift; then + log "✅ $target uses swiftSettings: defaultSwiftSettings" + else + error "❌ $target missing swiftSettings: defaultSwiftSettings" + fi + done fi # Directories if [ -d Sources ]; then - log "✅ Sources directory exists" + log "✅ Sources directory exists" else - error "❌ Sources directory missing" + error "❌ Sources directory missing" fi if [ -d Tests ]; then - log "✅ Tests directory exists" + log "✅ Tests directory exists" else - error "❌ Tests directory missing" + error "❌ Tests directory missing" fi # Required repository files @@ -151,14 +153,13 @@ check_file "LICENSE" check_file "Makefile" check_file "README.md" - # LICENSE must contain current year CURRENT_YEAR="$(date +%Y)" if grep -q "$CURRENT_YEAR" LICENSE; then - log "✅ LICENSE contains current year ($CURRENT_YEAR)" + log "✅ LICENSE contains current year ($CURRENT_YEAR)" else - error "❌ LICENSE does not contain current year ($CURRENT_YEAR)" + error "❌ LICENSE does not contain current year ($CURRENT_YEAR)" fi fatal_if_errors diff --git a/scripts/check-unacceptable-language.sh b/scripts/check-unacceptable-language.sh index b7871a1..3cedd15 100755 --- a/scripts/check-unacceptable-language.sh +++ b/scripts/check-unacceptable-language.sh @@ -18,9 +18,12 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent CI logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # List of unacceptable or discouraged words # The list is converted into a single regex later @@ -28,6 +31,10 @@ UNACCEPTABLE_WORD_LIST="blacklist whitelist slave master sane sanity insane insa # Will contain matches if any unacceptable language is found PATHS_WITH_UNACCEPTABLE_LANGUAGE="" +UNACCEPTABLE_PATTERN="${UNACCEPTABLE_WORD_LIST// /|}" + +# Build git grep pathspec args. Start with repository root selector. +set -- . # If an ignore file exists, use it to exclude paths from the search # @@ -36,28 +43,24 @@ PATHS_WITH_UNACCEPTABLE_LANGUAGE="" if [[ -f .unacceptablelanguageignore ]]; then log "Found unacceptablelanguageignore file..." log "Checking for unacceptable language..." - - PATHS_WITH_UNACCEPTABLE_LANGUAGE=$( - tr '\n' '\0' < .unacceptablelanguageignore \ - | xargs -0 -I% printf '":(exclude)%" ' \ - | xargs git grep -i -I -w -H -n --column -E "${UNACCEPTABLE_WORD_LIST// /|}" \ - | grep -v "ignore-unacceptable-language" \ - || true - ) | /usr/bin/paste -s -d " " - + while IFS= read -r path; do + [ -z "${path}" ] && continue + set -- "$@" ":(exclude)${path}" + done <.unacceptablelanguageignore else log "Checking for unacceptable language..." - - PATHS_WITH_UNACCEPTABLE_LANGUAGE=$( - git grep -i -I -w -H -n --column -E "${UNACCEPTABLE_WORD_LIST// /|}" \ - | grep -v "ignore-unacceptable-language" \ - || true - ) | /usr/bin/paste -s -d " " - fi +PATHS_WITH_UNACCEPTABLE_LANGUAGE="$( + git grep -i -I -w -H -n --column -E "${UNACCEPTABLE_PATTERN}" -- "$@" | + grep -v "ignore-unacceptable-language" || + true +)" + # If any matches were found, fail the script and print the affected files if [ -n "${PATHS_WITH_UNACCEPTABLE_LANGUAGE}" ]; then fatal "❌ Found unacceptable language in files: ${PATHS_WITH_UNACCEPTABLE_LANGUAGE}." fi # Success -log "✅ Found no unacceptable language." \ No newline at end of file +log "✅ Found no unacceptable language." diff --git a/scripts/generate-contributors-list.sh b/scripts/generate-contributors-list.sh index b29d701..d725d75 100644 --- a/scripts/generate-contributors-list.sh +++ b/scripts/generate-contributors-list.sh @@ -18,9 +18,12 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent CI and local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Resolve the repository root # Ensures the CONTRIBUTORS.txt file is always written at the top level @@ -28,13 +31,14 @@ REPO_ROOT="$(git -C "$PWD" rev-parse --show-toplevel)" # Collect contributors from git history # -# - `git shortlog -es` groups commits by author +# - `git shortlog -es HEAD` groups commits by author # - Output format: "\tName " -contributors=$(git shortlog -es 2>/dev/null) - -# If git shortlog fails, the repository is likely invalid -if [ $? -ne 0 ]; then - fatal "Error: Unable to run 'git shortlog'. Are you in a valid git repository?" +if git rev-parse --verify HEAD >/dev/null 2>&1; then + if ! contributors=$(git shortlog -es HEAD 2>/dev/null); then + fatal "Error: Unable to run 'git shortlog'. Are you in a valid git repository?" + fi +else + contributors="" fi # Handle the case where no contributors are found @@ -44,7 +48,7 @@ if [ -z "$contributors" ]; then exit 0 else # Strip commit counts and format as a Markdown list - contributors=$(echo "$contributors" | awk '{$1=""; print "- " $0}') + contributors=$(printf '%s\n' "$contributors" | awk '{$1=""; print "- " $0}') fi log "Creating CONTRIBUTORS.txt file..." @@ -59,7 +63,7 @@ log "Creating CONTRIBUTORS.txt file..." # The .mailmap file should be used to: # - Fix misspelled names # - Merge duplicate identities -cat > "$REPO_ROOT/CONTRIBUTORS.txt" <<- EOF +cat >"$REPO_ROOT/CONTRIBUTORS.txt" <<-EOF ### Contributors $contributors @@ -69,4 +73,4 @@ cat > "$REPO_ROOT/CONTRIBUTORS.txt" <<- EOF EOF # Success message -log "✅ CONTRIBUTORS.txt created with no errors." \ No newline at end of file +log "✅ CONTRIBUTORS.txt created with no errors." diff --git a/scripts/generate-docc.sh b/scripts/generate-docc.sh index 75bc8ff..d1459f3 100755 --- a/scripts/generate-docc.sh +++ b/scripts/generate-docc.sh @@ -18,9 +18,12 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent CI and local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Configuration / state OUTPUT_DIR="./docs" # Output directory for generated documentation @@ -34,7 +37,7 @@ REPO_NAME="" # Hosting base path (for GitHub Pages) # Swift package manifest to mutate PACKAGE_FILE="Package.swift" # Required injection anchor inside dependencies -INJECT_MARKER='// [docc-plugin-placeholder]' +INJECT_MARKER='// [docc-plugin-placeholder]' # Dependency line injected after the marker DOCC_DEP=' .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.0"),' @@ -128,13 +131,13 @@ ensure_docc_plugin() { END { if (!injected) exit 42 } - ' "$PACKAGE_FILE" > "$PACKAGE_FILE.tmp" + ' "$PACKAGE_FILE" >"$PACKAGE_FILE.tmp" mv "$PACKAGE_FILE.tmp" "$PACKAGE_FILE" # Validate manifest after mutation - swift package dump-package >/dev/null \ - || fatal "Package.swift became invalid after injecting swift-docc-plugin" + swift package dump-package >/dev/null || + fatal "Package.swift became invalid after injecting swift-docc-plugin" } # Restore git state after documentation generation (local only) @@ -186,7 +189,7 @@ load_from_config() { fi for TARGET in $TARGETS; do - TARGET_FLAGS+=( --target "$TARGET" ) + TARGET_FLAGS+=(--target "$TARGET") done } @@ -198,8 +201,8 @@ auto_detect_targets() { fatal "jq is required (install with: brew install jq)" fi - TARGETS=$(swift package dump-package \ - | jq -r '.targets[] + TARGETS=$(swift package dump-package | + jq -r '.targets[] | select(.type == "regular" or .type == "executable") | .name') @@ -208,7 +211,7 @@ auto_detect_targets() { fi for TARGET in $TARGETS; do - TARGET_FLAGS+=( --target "$TARGET" ) + TARGET_FLAGS+=(--target "$TARGET") done } @@ -233,7 +236,7 @@ generate_pages_redirects() { # ------------------------------------------------------------ # 1. Site root redirect → /documentation/ # ------------------------------------------------------------ - cat > "$OUTPUT_DIR/index.html" <"$OUTPUT_DIR/index.html" < @@ -259,13 +262,19 @@ EOF # 3. Single-target → redirect /documentation/ → /documentation// # ------------------------------------------------------------ local TARGET - TARGET=$(ls -d "$DOC_ROOT"/*/ 2>/dev/null | head -n 1 | xargs basename) + local FIRST_TARGET_DIR + FIRST_TARGET_DIR="$(find "$DOC_ROOT" -mindepth 1 -maxdepth 1 -type d -print -quit)" + if [ -n "$FIRST_TARGET_DIR" ]; then + TARGET="$(basename "$FIRST_TARGET_DIR")" + else + TARGET="" + fi if [ -z "$TARGET" ]; then fatal "Unable to determine single DocC target" fi - cat > "$DOC_ROOT/index.html" <"$DOC_ROOT/index.html" < @@ -323,8 +332,8 @@ if $LOCAL_MODE; then generate-documentation \ $COMBINED_FLAG \ "${TARGET_FLAGS[@]}" \ - --output-path "$OUTPUT_DIR" \ - || DOCS_EXIT_CODE=$? + --output-path "$OUTPUT_DIR" || + DOCS_EXIT_CODE=$? else swift package \ --allow-writing-to-directory "$OUTPUT_DIR" \ @@ -333,8 +342,8 @@ else "${TARGET_FLAGS[@]}" \ --output-path "$OUTPUT_DIR" \ --transform-for-static-hosting \ - ${REPO_NAME:+--hosting-base-path "$REPO_NAME"} \ - || DOCS_EXIT_CODE=$? + ${REPO_NAME:+--hosting-base-path "$REPO_NAME"} || + DOCS_EXIT_CODE=$? fi # Report failure without hiding the exit code @@ -348,4 +357,4 @@ fi # Cleanup and exit reset_git_after_docs -exit $DOCS_EXIT_CODE \ No newline at end of file +exit $DOCS_EXIT_CODE diff --git a/scripts/install-swift-openapi-generator.sh b/scripts/install-swift-openapi-generator.sh index 20938b0..49c9a7f 100755 --- a/scripts/install-swift-openapi-generator.sh +++ b/scripts/install-swift-openapi-generator.sh @@ -18,9 +18,12 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent CI and local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Repository containing the Swift OpenAPI Generator REPO="https://github.com/apple/swift-openapi-generator" @@ -29,9 +32,9 @@ REPO="https://github.com/apple/swift-openapi-generator" # # Tags are sorted using semantic version ordering to ensure # the newest release is selected. -VERSION=$(git ls-remote --tags --sort="v:refname" "${REPO}" \ - | tail -n1 \ - | sed 's/.*\///; s/\^{}//') +VERSION=$(git ls-remote --tags --sort="v:refname" "${REPO}" | + tail -n1 | + sed 's/.*\///; s/\^{}//') # Parse optional flags # @@ -41,8 +44,7 @@ while getopts v: flag; do v) VERSION=${OPTARG} ;; - *) - ;; + *) ;; esac done @@ -71,4 +73,4 @@ cd .. rm -f "${VERSION}.tar.gz" rm -rf "swift-openapi-generator-${VERSION}" -log "✅ swift-openapi-generator ${VERSION} installed successfully." \ No newline at end of file +log "✅ swift-openapi-generator ${VERSION} installed successfully." diff --git a/scripts/run-actionlint.sh b/scripts/run-actionlint.sh new file mode 100755 index 0000000..9737704 --- /dev/null +++ b/scripts/run-actionlint.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# GitHub Actions Workflow Lint Script +# +# Runs actionlint against repository workflows. + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { + error "$@" + exit 1 +} + +if ! command -v actionlint >/dev/null 2>&1; then + fatal "actionlint is not installed. Install it first (e.g. 'brew install actionlint' or 'apt-get install actionlint')." +fi + +REPO_ROOT="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$REPO_ROOT" + +log "Running actionlint..." +actionlint "$@" +log "✅ actionlint found no issues." diff --git a/scripts/run-clean.sh b/scripts/run-clean.sh index bb38111..4cca511 100755 --- a/scripts/run-clean.sh +++ b/scripts/run-clean.sh @@ -17,13 +17,16 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Remove files and directories rm -rf ".build" rm -rf ".swiftpm" rm -f "openapi/openapi.yaml" rm -f "db.sqlite" -rm -f "migration-entries.json" \ No newline at end of file +rm -f "migration-entries.json" diff --git a/scripts/run-docc-docker.sh b/scripts/run-docc-docker.sh index f844aa0..4b9e46e 100755 --- a/scripts/run-docc-docker.sh +++ b/scripts/run-docc-docker.sh @@ -17,9 +17,12 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Resolve repository root # Ensures the docs directory is located correctly even if the script @@ -70,4 +73,4 @@ echo docker run --rm --name "${NAME}" \ -v "${DOCC_DIR}:/usr/share/nginx/html" \ -p "${PORT}" \ - nginx \ No newline at end of file + nginx diff --git a/scripts/run-openapi-docker.sh b/scripts/run-openapi-docker.sh index 117260a..6f8abf2 100755 --- a/scripts/run-openapi-docker.sh +++ b/scripts/run-openapi-docker.sh @@ -16,24 +16,17 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} -# Resolve repository root -# Allows the script to be executed from any subdirectory -REPO_ROOT="$(git -C "$PWD" rev-parse --show-toplevel)" - -# Location of OpenAPI files -# The directory is expected to contain one or more OpenAPI specifications -OPENAPI_YAML_LOCATION="${REPO_ROOT}/openapi" - -# If the OpenAPI directory does not exist, skip serving gracefully -# This avoids failing for repositories without API definitions -if [ ! -d "${OPENAPI_YAML_LOCATION}" ]; then - error "❗ OpenAPI location not found." - exit 0 -fi +# Resolve script directory +# Allows the script to be executed from any subdirectory. +SCRIPT_SOURCE="${BASH_SOURCE[0]-$0}" +SCRIPT_DIR="$(cd -- "$(dirname -- "${SCRIPT_SOURCE}")" && pwd)" # Default Docker container name NAME="openapi-server" @@ -42,24 +35,106 @@ NAME="openapi-server" # Nginx listens on port 80 inside the container PORT="8888:80" +# Default OpenAPI path (file or directory) +# If a file is provided, its parent directory will be mounted. +OPENAPI_PATH="openapi" + +resolve_repo_root() { + # Prefer git root for local execution and subdirectory calls. + if root="$(git rev-parse --show-toplevel 2>/dev/null)"; then + printf '%s\n' "${root}" + return 0 + fi + + # Fallback for checked-out scripts under a conventional ./scripts layout. + if [ -d "${SCRIPT_DIR}/../.git" ]; then + ( + cd -- "${SCRIPT_DIR}/.." + pwd + ) + return 0 + fi + + # Last resort for piped execution (for example: curl | bash). + pwd +} + +usage() { + cat >&2 <&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Determine swift-format command mode # @@ -33,9 +36,9 @@ fatal() { error "$@"; exit 1; } # If --fix is provided, switch to in-place formatting. FORMAT_COMMAND=(lint --strict) for arg in "$@"; do - if [ "$arg" == "--fix" ]; then - FORMAT_COMMAND=(format --in-place) - fi + if [ "$arg" == "--fix" ]; then + FORMAT_COMMAND=(format --in-place) + fi done # Base URL for shared swift-format configuration files @@ -67,19 +70,19 @@ fi # - Runs formatting in parallel for performance # # The exit code is captured explicitly to allow controlled error handling. -tr '\n' '\0' < .swiftformatignore \ -| xargs -0 -I% printf '":(exclude)%" ' \ -| xargs git ls-files -z '*.swift' \ -| xargs -0 swift format "${FORMAT_COMMAND[@]}" --parallel \ -&& SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? +tr '\n' '\0' <.swiftformatignore | + xargs -0 -I% printf '":(exclude)%" ' | + xargs git ls-files -z '*.swift' | + xargs -0 swift format "${FORMAT_COMMAND[@]}" --parallel && + SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? # If swift-format failed, print a helpful error message if [ "${SWIFT_FORMAT_RC}" -ne 0 ]; then - fatal "❌ Running swift-format produced errors. + fatal "❌ Running swift-format produced errors. To fix: % run make format " fi # Success -log "✅ Ran swift-format with no errors." \ No newline at end of file +log "✅ Ran swift-format with no errors." diff --git a/scripts/script-format.sh b/scripts/script-format.sh new file mode 100755 index 0000000..a8fd365 --- /dev/null +++ b/scripts/script-format.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Shell Script Format Check / Fix Script +# +# This script formats shell-related files with shfmt. +# +# Default behavior: +# - Runs shfmt in diff mode and fails when formatting drift is found. +# +# Optional behavior: +# - If --fix is passed, writes formatting changes in-place. + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { + error "$@" + exit 1 +} + +usage() { + cat >&2 </dev/null 2>&1; then + fatal "shfmt is not installed. Install it first (e.g. 'brew install shfmt' or 'apt-get install shfmt')." +fi + +FILES=() +while IFS= read -r -d '' file; do + FILES+=("$file") +done < <(git ls-files -z '*.sh' '*.bash' '*.bats') + +if [ "${#FILES[@]}" -eq 0 ]; then + log "No shell files found to format." + exit 0 +fi + +if [ "$FIX_MODE" -eq 1 ]; then + shfmt -w -i 4 -ci "${FILES[@]}" + log "✅ Applied shfmt formatting." + exit 0 +fi + +set +e +shfmt -d -i 4 -ci "${FILES[@]}" +SHFMT_RC=$? +set -e + +if [ "$SHFMT_RC" -ne 0 ]; then + fatal "shfmt found formatting issues. Run with --fix to apply changes." +fi + +log "✅ shfmt found no formatting issues." diff --git a/tests/bats/check-broken-symlinks.bats b/tests/bats/check-broken-symlinks.bats new file mode 100755 index 0000000..1c20a11 --- /dev/null +++ b/tests/bats/check-broken-symlinks.bats @@ -0,0 +1,64 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "check-broken-symlinks passes when all symlinks are valid" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-broken-symlinks.sh" + + cat >"$repo/target.txt" <<'TXT' +hello +TXT + ln -s target.txt "$repo/link.txt" + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-broken-symlinks.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 broken symlinks"* ]] +} + +@test "check-broken-symlinks fails when symlink target is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-broken-symlinks.sh" + + ln -s missing.txt "$repo/broken.txt" + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-broken-symlinks.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Broken symlink: broken.txt"* ]] +} + +@test "check-broken-symlinks ignores regular files even if they mention missing paths" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-broken-symlinks.sh" + + cat >"$repo/notes.txt" <<'TXT' +This mentions a missing file path: ./does-not-exist.txt +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-broken-symlinks.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 broken symlinks"* ]] +} + +@test "check-broken-symlinks checks only tracked symlinks" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-broken-symlinks.sh" + + cat >"$repo/tracked.txt" <<'TXT' +ok +TXT + git -C "$repo" add tracked.txt + git -C "$repo" commit -q -m "baseline" + ln -s missing.txt "$repo/untracked-broken.txt" + + run bash -c "cd '$repo' && bash scripts/check-broken-symlinks.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 broken symlinks"* ]] +} diff --git a/tests/bats/check-local-swift-dependencies.bats b/tests/bats/check-local-swift-dependencies.bats new file mode 100755 index 0000000..cf10dc9 --- /dev/null +++ b/tests/bats/check-local-swift-dependencies.bats @@ -0,0 +1,95 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "check-local-swift-dependencies passes when no .package(path:) is present" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-local-swift-dependencies.sh" + + cat >"$repo/Package.swift" <<'SWIFT' +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "Demo", + dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0") + ], + targets: [] +) +SWIFT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-local-swift-dependencies.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 local Swift package dependency references"* ]] +} + +@test "check-local-swift-dependencies fails when .package(path:) exists" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-local-swift-dependencies.sh" + + cat >"$repo/Package.swift" <<'SWIFT' +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "Demo", + dependencies: [ + .package(path: "../LocalPackage") + ], + targets: [] +) +SWIFT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-local-swift-dependencies.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"contains local Swift package reference"* ]] +} + +@test "check-local-swift-dependencies ignores untracked Package.swift" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-local-swift-dependencies.sh" + + cat >"$repo/Package.swift" <<'SWIFT' +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "Demo", + dependencies: [ + .package(path: "../LocalPackage") + ], + targets: [] +) +SWIFT + cat >"$repo/README.md" <<'TXT' +fixture +TXT + git -C "$repo" add README.md + git -C "$repo" commit -q -m "tracked file only" + + run bash -c "cd '$repo' && bash scripts/check-local-swift-dependencies.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 local Swift package dependency references"* ]] +} + +@test "check-local-swift-dependencies passes when no Package.swift is tracked" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-local-swift-dependencies.sh" + + cat >"$repo/README.md" <<'TXT' +fixture +TXT + git -C "$repo" add README.md + git -C "$repo" commit -q -m "no package swift" + + run bash -c "cd '$repo' && bash scripts/check-local-swift-dependencies.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 local Swift package dependency references"* ]] +} diff --git a/tests/bats/check-swift-headers.bats b/tests/bats/check-swift-headers.bats new file mode 100755 index 0000000..362aa43 --- /dev/null +++ b/tests/bats/check-swift-headers.bats @@ -0,0 +1,294 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME}")/../.." && pwd)" + +setup() { + TMP_ROOT="$(mktemp -d)" + TMPDIR="$TMP_ROOT/HeaderProject" + mkdir -p "$TMPDIR" + + copy_script_into_repo "$TMPDIR" "check-swift-headers.sh" +} + +teardown() { + rm -rf "$TMP_ROOT" +} + +init_git() { + git -C "$TMPDIR" init -q + git -C "$TMPDIR" config user.name "Test User" + git -C "$TMPDIR" config user.email "test@example.com" + git -C "$TMPDIR" add -A + GIT_AUTHOR_DATE="2026-02-12T12:00:00Z" \ + GIT_COMMITTER_DATE="2026-02-12T12:00:00Z" \ + git -C "$TMPDIR" commit -q -m "init fixtures" +} + +run_checker() { + (cd "$TMPDIR" && bash scripts/check-swift-headers.sh "$@") +} + +@test "valid header with single dot passes without changes" { + cp "$REPO_ROOT/tests/fixtures/valid_single_dot.swift" \ + "$TMPDIR/Address.swift" + + init_git + + run run_checker + [ "$status" -eq 0 ] + + diff -u \ + "$REPO_ROOT/tests/fixtures/valid_single_dot.swift" \ + "$TMPDIR/Address.swift" +} + +@test "legacy double-dot header is normalized but not duplicated" { + cp "$REPO_ROOT/tests/fixtures/legacy_double_dot.swift" \ + "$TMPDIR/Address.swift" + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + diff -u \ + "$REPO_ROOT/tests/fixtures/legacy_double_dot_fixed.swift" \ + "$TMPDIR/Address.swift" + + run run_checker + [ "$status" -eq 0 ] +} + +@test "wrong project name is fixed but author and date are preserved" { + cp "$REPO_ROOT/tests/fixtures/wrong_project.swift" \ + "$TMPDIR/WrongProject.swift" + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + diff -u \ + "$REPO_ROOT/tests/fixtures/wrong_project_fixed.swift" \ + "$TMPDIR/WrongProject.swift" + + run run_checker + [ "$status" -eq 0 ] +} + +@test "wrong project name fails in check mode" { + cp "$REPO_ROOT/tests/fixtures/wrong_project.swift" \ + "$TMPDIR/WrongProject.swift" + + init_git + + run run_checker + [ "$status" -ne 0 ] + [[ "$output" == *"Header is invalid"* ]] +} + +@test "missing header fails in check mode" { + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Foo.swift" + + init_git + + run run_checker + [ "$status" -ne 0 ] +} + +@test "missing header with leading random comments fails in check mode" { + cat >"$TMPDIR/Foo.swift" <<'EOF2' +// random comment one +// random comment two +import Foundation + +struct Foo { + let value: Int +} +EOF2 + + init_git + + run run_checker + [ "$status" -ne 0 ] + [[ "$output" == *"Header missing"* ]] +} + +@test "missing header is inserted in fix mode" { + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Foo.swift" + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + diff -u \ + "$REPO_ROOT/tests/fixtures/missing_header_fixed.swift" \ + "$TMPDIR/Foo.swift" + + run run_checker + [ "$status" -eq 0 ] +} + +@test "missing header with leading random comments is fixed in fix mode" { + cat >"$TMPDIR/Foo.swift" <<'EOF2' +// random comment one +// random comment two +import Foundation + +struct Foo { + let value: Int +} +EOF2 + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + content="$(cat "$TMPDIR/Foo.swift")" + [[ "$content" == *"// Foo.swift"* ]] + [[ "$content" == *"// Created by Binary Birds on"* ]] + [[ "$content" == *"// random comment one"* ]] + [[ "$content" == *"// random comment two"* ]] + + run run_checker + [ "$status" -eq 0 ] +} + +@test "fix mode is idempotent" { + cp "$REPO_ROOT/tests/fixtures/legacy_double_dot.swift" \ + "$TMPDIR/Address.swift" + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + cp "$TMPDIR/Address.swift" "$TMPDIR/once.swift" + + run run_checker --fix + [ "$status" -eq 0 ] + + diff -u "$TMPDIR/once.swift" "$TMPDIR/Address.swift" +} + +@test "--author without value fails with clear error" { + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Foo.swift" + + init_git + + run run_checker --fix --author + [ "$status" -ne 0 ] + [[ "$output" == *"--author requires a value"* ]] +} + +@test "unknown argument fails with clear error" { + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Foo.swift" + + init_git + + run run_checker --unknown + [ "$status" -ne 0 ] + [[ "$output" == *"Unknown argument: --unknown"* ]] +} + +@test "Package.swift is skipped even without header" { + cat >"$TMPDIR/Package.swift" <<'EOF2' +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "HeaderProject", + targets: [] +) +EOF2 + cp "$REPO_ROOT/tests/fixtures/valid_single_dot.swift" \ + "$TMPDIR/Address.swift" + + init_git + + run run_checker + [ "$status" -eq 0 ] +} + +@test "invalid header fails in check mode" { + cat >"$TMPDIR/Bad.swift" <<'EOF2' +// +// WrongName.swift +// HeaderProject +// +// Created by Test User on 2026. 02. 12. + +struct Bad {} +EOF2 + + init_git + + run run_checker + [ "$status" -ne 0 ] + [[ "$output" == *"Header is invalid"* ]] +} + +@test "invalid header is fixed while preserving author and date" { + cat >"$TMPDIR/Preserve.swift" <<'EOF2' +// +// WrongName.swift +// WrongProject +// +// Created by Alice Doe on 2026. 02. 12. + +struct Preserve {} +EOF2 + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + content="$(cat "$TMPDIR/Preserve.swift")" + [[ "$content" == *"// Preserve.swift"* ]] + [[ "$content" == *"// HeaderProject"* ]] + [[ "$content" == *"// Created by Alice Doe on 2026. 02. 12."* ]] + + run run_checker + [ "$status" -eq 0 ] +} + +@test ".swiftheaderignore supports comments and blank lines" { + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Ignored.swift" + cp "$REPO_ROOT/tests/fixtures/valid_single_dot.swift" \ + "$TMPDIR/Address.swift" + cat >"$TMPDIR/.swiftheaderignore" <<'EOF2' +# Ignore this swift file +Ignored.swift + +# Also ignore helper shell scripts +scripts/** +.swiftheaderignore +EOF2 + + init_git + + run run_checker + [ "$status" -eq 0 ] +} + +@test "running from subdirectory behaves the same" { + mkdir -p "$TMPDIR/Sources" + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Sources/Foo.swift" + + init_git + + run bash -c "cd '$TMPDIR/Sources' && ../scripts/check-swift-headers.sh" + [ "$status" -ne 0 ] + [[ "$output" == *"Header missing"* ]] +} diff --git a/tests/bats/check-swift-package.bats b/tests/bats/check-swift-package.bats new file mode 100755 index 0000000..029a9e6 --- /dev/null +++ b/tests/bats/check-swift-package.bats @@ -0,0 +1,264 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +setup_valid_swift_package_layout() { + local repo="$1" + local year + year="$(date +%Y)" + + mkdir -p "$repo/Sources/Demo" "$repo/Tests/DemoTests" + cat >"$repo/.swift-format" <<'EOF' +{} +EOF + : >"$repo/.swiftformatignore" + cat >"$repo/Makefile" <<'EOF' +all: + @echo ok +EOF + cat >"$repo/README.md" <<'EOF' +# Demo +EOF + cat >"$repo/LICENSE" <"$repo/Package.swift" <<'EOF' +// swift-tools-version: 6.1 +import PackageDescription + +let defaultSwiftSettings: [SwiftSetting] = [ + .enableUpcomingFeature("NonisolatedNonsendingByDefault") +] + +let package = Package( + name: "Demo", + products: [ + .library(name: "Demo", targets: ["Demo"]) + ], + dependencies: [ + // [docc-plugin-placeholder] + ], + targets: [ + .target( + name: "Demo", + swiftSettings: defaultSwiftSettings + ), + .testTarget( + name: "DemoTests", + dependencies: ["Demo"], + swiftSettings: defaultSwiftSettings + ) + ] +) +EOF +} + +install_swift_dump_stub_success() { + local repo="$1" + + mkdir -p "$repo/fakebin" + cat >"$repo/fakebin/swift" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +if [ "${1:-}" = "package" ] && [ "${2:-}" = "dump-package" ]; then + cat <<'JSON' +{"dependencies":[],"targets":[{"name":"Demo"},{"name":"DemoTests"}]} +JSON + exit 0 +fi +exit 1 +EOF + chmod +x "$repo/fakebin/swift" + export PATH="$repo/fakebin:$PATH" +} + +install_swift_dump_stub_failure() { + local repo="$1" + + mkdir -p "$repo/fakebin" + cat >"$repo/fakebin/swift" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +if [ "${1:-}" = "package" ] && [ "${2:-}" = "dump-package" ]; then + exit 1 +fi +exit 1 +EOF + chmod +x "$repo/fakebin/swift" + export PATH="$repo/fakebin:$PATH" +} + +@test "check-swift-package fails clearly when Package.swift is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Package.swift missing"* ]] +} + +@test "check-swift-package passes on valid package structure and settings" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Swift Package validation passed"* ]] +} + +@test "check-swift-package fails when swift-tools-version is below 6.1" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + sed -i.bak 's|// swift-tools-version: 6.1|// swift-tools-version: 5.9|' "$repo/Package.swift" + rm -f "$repo/Package.swift.bak" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"swift-tools-version too old"* ]] +} + +@test "check-swift-package fails when docc placeholder is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + sed -i.bak '/docc-plugin-placeholder/d' "$repo/Package.swift" + rm -f "$repo/Package.swift.bak" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"docc plugin placeholder missing"* ]] +} + +@test "check-swift-package fails when a target misses swiftSettings: defaultSwiftSettings" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + cat >"$repo/Package.swift" <<'EOF' +// swift-tools-version: 6.1 +import PackageDescription + +let defaultSwiftSettings: [SwiftSetting] = [ + .enableUpcomingFeature("NonisolatedNonsendingByDefault") +] + +let package = Package( + name: "Demo", + products: [ + .library(name: "Demo", targets: ["Demo"]) + ], + dependencies: [ + // [docc-plugin-placeholder] + ], + targets: [ + .target( + name: "Demo", + swiftSettings: defaultSwiftSettings + ), + .testTarget( + name: "DemoTests", + dependencies: ["Demo"] + ) + ] +) +EOF + install_swift_dump_stub_success "$repo" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"DemoTests missing swiftSettings: defaultSwiftSettings"* ]] +} + +@test "check-swift-package fails when LICENSE does not contain current year" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + cat >"$repo/LICENSE" <<'EOF' +Copyright 2001 +EOF + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"LICENSE does not contain current year"* ]] +} + +@test "check-swift-package fails when Sources directory is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + rm -rf "$repo/Sources" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Sources directory missing"* ]] +} + +@test "check-swift-package fails when Tests directory is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + rm -rf "$repo/Tests" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Tests directory missing"* ]] +} + +@test "check-swift-package fails when swift package dump-package fails" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_failure "$repo" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Failed to parse Package.swift via SwiftPM"* ]] +} + +@test "check-swift-package fails when NonisolatedNonsendingByDefault is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + sed -i.bak '/NonisolatedNonsendingByDefault/d' "$repo/Package.swift" + rm -f "$repo/Package.swift.bak" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"NonisolatedNonsendingByDefault missing"* ]] +} diff --git a/tests/bats/check-unacceptable-language.bats b/tests/bats/check-unacceptable-language.bats new file mode 100755 index 0000000..5d74e4c --- /dev/null +++ b/tests/bats/check-unacceptable-language.bats @@ -0,0 +1,109 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "check-unacceptable-language passes when no banned terms exist" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/good.txt" <<'TXT' +This file uses inclusive and neutral wording. +TXT + commit_all "$repo" + + cat >"$repo/.unacceptablelanguageignore" <<'TXT' +scripts/check-unacceptable-language.sh +TXT + commit_all "$repo" "add ignore file" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found no unacceptable language"* ]] +} + +@test "check-unacceptable-language fails when banned terms exist" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/bad.txt" <<'TXT' +This line contains blacklist and should fail. +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Found unacceptable language"* ]] + [[ "$output" == *"bad.txt"* ]] +} + +@test "check-unacceptable-language respects .unacceptablelanguageignore" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/bad.txt" <<'TXT' +This line contains blacklist. +TXT + cat >"$repo/.unacceptablelanguageignore" <<'TXT' +bad.txt +scripts/check-unacceptable-language.sh +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found no unacceptable language"* ]] +} + +@test "check-unacceptable-language ignores lines with ignore-unacceptable-language marker" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/allowed.txt" <<'TXT' +blacklist // ignore-unacceptable-language +TXT + cat >"$repo/.unacceptablelanguageignore" <<'TXT' +scripts/check-unacceptable-language.sh +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found no unacceptable language"* ]] +} + +@test "check-unacceptable-language handles an empty ignore file" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/good.txt" <<'TXT' +Inclusive wording only. +TXT + : >"$repo/.unacceptablelanguageignore" + git -C "$repo" add good.txt .unacceptablelanguageignore + git -C "$repo" commit -q -m "add fixtures" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found no unacceptable language"* ]] +} + +@test "check-unacceptable-language matches whole words only" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/words.txt" <<'TXT' +mastery and blacklisting should not match whole-word checks. +TXT + git -C "$repo" add words.txt + git -C "$repo" commit -q -m "add words fixture" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found no unacceptable language"* ]] +} diff --git a/tests/bats/generate-contributors-list.bats b/tests/bats/generate-contributors-list.bats new file mode 100755 index 0000000..dab7716 --- /dev/null +++ b/tests/bats/generate-contributors-list.bats @@ -0,0 +1,65 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "generate-contributors-list creates CONTRIBUTORS.txt from git history" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "generate-contributors-list.sh" + + cat >"$repo/file.txt" <<'TXT' +content +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/generate-contributors-list.sh" + + [ "$status" -eq 0 ] + [ -f "$repo/CONTRIBUTORS.txt" ] + + contributors_content="$(cat "$repo/CONTRIBUTORS.txt")" + [[ "$contributors_content" == *"### Contributors"* ]] + [[ "$contributors_content" == *"Test User "* ]] +} + +@test "generate-contributors-list exits cleanly in repository with no commits" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "generate-contributors-list.sh" + + run bash -c "cd '$repo' && bash scripts/generate-contributors-list.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"No contributors found"* ]] + [ ! -f "$repo/CONTRIBUTORS.txt" ] +} + +@test "generate-contributors-list writes CONTRIBUTORS.txt at repo root from subdirectory" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "generate-contributors-list.sh" + mkdir -p "$repo/subdir" + + cat >"$repo/file.txt" <<'TXT' +content +TXT + commit_all "$repo" + + run bash -c "cd '$repo/subdir' && bash ../scripts/generate-contributors-list.sh" + + [ "$status" -eq 0 ] + [ -f "$repo/CONTRIBUTORS.txt" ] +} + +@test "generate-contributors-list output includes mailmap guidance" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "generate-contributors-list.sh" + + cat >"$repo/file.txt" <<'TXT' +content +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/generate-contributors-list.sh" + + [ "$status" -eq 0 ] + contributors_content="$(cat "$repo/CONTRIBUTORS.txt")" + [[ "$contributors_content" == *"./.mailmap"* ]] +} diff --git a/tests/bats/helpers/test_helper.bash b/tests/bats/helpers/test_helper.bash new file mode 100755 index 0000000..5293b8d --- /dev/null +++ b/tests/bats/helpers/test_helper.bash @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +PROJECT_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME}")/../.." && pwd)" + +make_temp_repo() { + local repo + repo="$(mktemp -d)" + + git -C "$repo" init -q + git -C "$repo" config user.name "Test User" + git -C "$repo" config user.email "test@example.com" + + printf '%s\n' "$repo" +} + +copy_script_into_repo() { + local repo="$1" + local script_name="$2" + + mkdir -p "$repo/scripts" + cp "$PROJECT_ROOT/scripts/$script_name" "$repo/scripts/$script_name" + chmod +x "$repo/scripts/$script_name" +} + +copy_openapi_scripts_into_repo() { + local repo="$1" + copy_script_into_repo "$repo" "check-openapi-validation.sh" + copy_script_into_repo "$repo" "check-openapi-security.sh" + copy_script_into_repo "$repo" "run-openapi-docker.sh" +} + +install_docker_stub() { + local repo="$1" + + mkdir -p "$repo/fakebin" + cat >"$repo/fakebin/docker" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +: "${DOCKER_LOG:?DOCKER_LOG is required}" +printf '%s\n' "$*" >> "$DOCKER_LOG" +exit "${DOCKER_EXIT_CODE:-0}" +STUB + chmod +x "$repo/fakebin/docker" + + export PATH="$repo/fakebin:$PATH" +} + +commit_all() { + local repo="$1" + local message="${2:-test commit}" + + git -C "$repo" add -A + git -C "$repo" commit -q -m "$message" +} diff --git a/tests/bats/openapi-scripts.bats b/tests/bats/openapi-scripts.bats new file mode 100755 index 0000000..cd93b49 --- /dev/null +++ b/tests/bats/openapi-scripts.bats @@ -0,0 +1,217 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "check-openapi-validation resolves .yml to .yaml and calls docker with expected mount" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/api" + cat >"$repo/api/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh -f '$repo/api/openapi.yml'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/api/openapi.yaml:/openapi.yaml"* ]] + [[ "$docker_call" == *"pythonopenapi/openapi-spec-validator /openapi.yaml"* ]] +} + +@test "check-openapi-security exits successfully when OpenAPI path is missing" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-security.sh -f '$repo/does-not-exist'" + + [ "$status" -eq 0 ] + [[ "$output" == *"skipping security scan"* ]] + [ ! -f "$DOCKER_LOG" ] +} + +@test "run-openapi-docker mounts parent directory for file input and respects name/port" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/spec" + cat >"$repo/spec/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Preview API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/run-openapi-docker.sh -n preview -p 9999:80 -f '$repo/spec/openapi.yaml'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"--name preview"* ]] + [[ "$docker_call" == *"-v $repo/spec:/usr/share/nginx/html"* ]] + [[ "$docker_call" == *"-p 9999:80 nginx"* ]] +} + +@test "check-openapi-validation with directory prefers openapi.yaml over openapi.yml" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi" + cat >"$repo/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: {title: YAML, version: 1.0.0} +paths: {} +YAML + cat >"$repo/openapi/openapi.yml" <<'YAML' +openapi: 3.0.0 +info: {title: YML, version: 1.0.0} +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh -f '$repo/openapi'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/openapi/openapi.yaml:/openapi.yaml"* ]] +} + +@test "check-openapi-validation exits successfully when directory has no openapi spec" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi-empty" + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh -f '$repo/openapi-empty'" + + [ "$status" -eq 0 ] + [[ "$output" == *"skipping validation"* ]] + [ ! -f "$DOCKER_LOG" ] +} + +@test "check-openapi-security returns non-zero when docker scan fails" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi" + cat >"$repo/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + export DOCKER_EXIT_CODE=1 + run bash -c "cd '$repo' && bash scripts/check-openapi-security.sh -f '$repo/openapi/openapi.yaml'" + + [ "$status" -eq 1 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"zap-api-scan.py"* ]] +} + +@test "check-openapi-validation resolves default openapi path from repo root when run from subdirectory" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi" "$repo/subdir" + cat >"$repo/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Root API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo/subdir' && bash ../scripts/check-openapi-validation.sh" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"/openapi/openapi.yaml:/openapi.yaml"* ]] +} + +@test "check-openapi-validation works when script is piped to bash" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi" + cat >"$repo/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Pipe API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && cat scripts/check-openapi-validation.sh | bash" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"/openapi/openapi.yaml:/openapi.yaml"* ]] +} + +@test "check-openapi-validation supports piped execution with -f path relative to git root" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/mail-examples/mail-example-openapi/openapi" + cat >"$repo/mail-examples/mail-example-openapi/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Nested API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && cat scripts/check-openapi-validation.sh | bash -s -- -f mail-examples/mail-example-openapi/openapi/openapi.yaml" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"/mail-examples/mail-example-openapi/openapi/openapi.yaml:/openapi.yaml"* ]] +} + +@test "check-openapi-validation default path works for copied script in nested project scripts folder" { + repo="$(make_temp_repo)" + install_docker_stub "$repo" + + mkdir -p "$repo/mail-examples/mail-example-openapi/scripts" + mkdir -p "$repo/mail-examples/mail-example-openapi/openapi" + cp "$PROJECT_ROOT/scripts/check-openapi-validation.sh" \ + "$repo/mail-examples/mail-example-openapi/scripts/check-openapi-validation.sh" + chmod +x "$repo/mail-examples/mail-example-openapi/scripts/check-openapi-validation.sh" + + cat >"$repo/mail-examples/mail-example-openapi/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Nested Local API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo/mail-examples/mail-example-openapi' && bash ./scripts/check-openapi-validation.sh" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"/mail-example-openapi/openapi/openapi.yaml:/openapi.yaml"* ]] +} diff --git a/tests/fixtures/legacy_double_dot.swift b/tests/fixtures/legacy_double_dot.swift new file mode 100644 index 0000000..1261a16 --- /dev/null +++ b/tests/fixtures/legacy_double_dot.swift @@ -0,0 +1,11 @@ +// +// Address.swift +// HeaderProject +// +// Created by Test User on 2026. 02. 12.. + +import Foundation + +struct Address { + let city: String +} diff --git a/tests/fixtures/legacy_double_dot_fixed.swift b/tests/fixtures/legacy_double_dot_fixed.swift new file mode 100644 index 0000000..2e79148 --- /dev/null +++ b/tests/fixtures/legacy_double_dot_fixed.swift @@ -0,0 +1,11 @@ +// +// Address.swift +// HeaderProject +// +// Created by Test User on 2026. 02. 12. + +import Foundation + +struct Address { + let city: String +} diff --git a/tests/fixtures/missing_header.swift b/tests/fixtures/missing_header.swift new file mode 100644 index 0000000..6413cbf --- /dev/null +++ b/tests/fixtures/missing_header.swift @@ -0,0 +1,5 @@ +import Foundation + +struct Foo { + let value: Int +} diff --git a/tests/fixtures/missing_header_fixed.swift b/tests/fixtures/missing_header_fixed.swift new file mode 100644 index 0000000..f1aa9bd --- /dev/null +++ b/tests/fixtures/missing_header_fixed.swift @@ -0,0 +1,11 @@ +// +// Foo.swift +// HeaderProject +// +// Created by Binary Birds on 2026. 02. 12. + +import Foundation + +struct Foo { + let value: Int +} diff --git a/tests/fixtures/valid_single_dot.swift b/tests/fixtures/valid_single_dot.swift new file mode 100644 index 0000000..2e79148 --- /dev/null +++ b/tests/fixtures/valid_single_dot.swift @@ -0,0 +1,11 @@ +// +// Address.swift +// HeaderProject +// +// Created by Test User on 2026. 02. 12. + +import Foundation + +struct Address { + let city: String +} diff --git a/tests/fixtures/wrong_project.swift b/tests/fixtures/wrong_project.swift new file mode 100644 index 0000000..64b5b5f --- /dev/null +++ b/tests/fixtures/wrong_project.swift @@ -0,0 +1,11 @@ +// +// WrongProject.swift +// WrongProjectName +// +// Created by Test User on 2026. 02. 12. + +import Foundation + +struct WrongProject { + let value: String +} diff --git a/tests/fixtures/wrong_project_fixed.swift b/tests/fixtures/wrong_project_fixed.swift new file mode 100644 index 0000000..955dbd6 --- /dev/null +++ b/tests/fixtures/wrong_project_fixed.swift @@ -0,0 +1,11 @@ +// +// WrongProject.swift +// HeaderProject +// +// Created by Test User on 2026. 02. 12. + +import Foundation + +struct WrongProject { + let value: String +}