diff --git a/.conform.yaml b/.conform.yaml deleted file mode 100644 index ae0ec83..0000000 --- a/.conform.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Digg - Agency for Digital Government -# -# SPDX-License-Identifier: CC0-1.0 -policies: - - type: commit - spec: - dco: true - gpg: - required: true - header: - length: 90 - imperative: true - case: lower - invalidLastCharacters: . - body: - required: false - conventional: - types: ["feat", "fix", "build", "chore", "ci", "docs", "perf", "refactor", "revert", "style", "test", "release"] - scopes: [".*"] diff --git a/.github/workflows/openssf-scorecard.yml b/.github/workflows/openssf-scorecard.yml index b746dfd..1be86f2 100644 --- a/.github/workflows/openssf-scorecard.yml +++ b/.github/workflows/openssf-scorecard.yml @@ -17,6 +17,6 @@ jobs: contents: read security-events: write id-token: write - uses: diggsweden/reusable-ci/.github/workflows/security-openssf-scorecard.yml@e1e1387d5b0399bb5edb00e40485746772344176 # v2.6.0 + uses: diggsweden/reusable-ci/.github/workflows/security-openssf-scorecard.yml@659cc5dbdbedc47f1510817f38aba07de8a93ae8 # v2.6.1 with: publish-results: true diff --git a/.github/workflows/pull-request-workflow.yml b/.github/workflows/pull-request-workflow.yml index 9d7a884..ba44d3a 100644 --- a/.github/workflows/pull-request-workflow.yml +++ b/.github/workflows/pull-request-workflow.yml @@ -11,7 +11,7 @@ concurrency: cancel-in-progress: true jobs: pr-checks: - uses: diggsweden/reusable-ci/.github/workflows/pullrequest-orchestrator.yml@e1e1387d5b0399bb5edb00e40485746772344176 # v2.6.0 + uses: diggsweden/reusable-ci/.github/workflows/pullrequest-orchestrator.yml@659cc5dbdbedc47f1510817f38aba07de8a93ae8 # v2.6.1 permissions: contents: read packages: read diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index e64e656..d98f687 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -16,7 +16,7 @@ permissions: contents: read jobs: release: - uses: diggsweden/reusable-ci/.github/workflows/release-orchestrator.yml@e1e1387d5b0399bb5edb00e40485746772344176 # v2.6.0 + uses: diggsweden/reusable-ci/.github/workflows/release-orchestrator.yml@659cc5dbdbedc47f1510817f38aba07de8a93ae8 # v2.6.1 permissions: contents: write packages: write diff --git a/.gitignore b/.gitignore index e6e7954..32d599b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ tests/libs/ + +#IntelliJ +/*.iml +/.idea/ diff --git a/.gommitlint.yaml b/.gommitlint.yaml new file mode 100644 index 0000000..ec68bb0 --- /dev/null +++ b/.gommitlint.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2025 Digg - Agency for Digital Government +# +# SPDX-License-Identifier: CC0-1.0 + +gommitlint: + crypto_signature: + required: true + require_ssh: true + repo: + max_commits_ahead: 10 diff --git a/.mise.toml b/.mise.toml index da528a6..5eafb58 100644 --- a/.mise.toml +++ b/.mise.toml @@ -21,7 +21,7 @@ PIP_INDEX_URL = "{{ get_env(name='PIP_INDEX_URL', default='') }}" [tools] "aqua:rhysd/actionlint" = "v1.7.11" -"aqua:siderolabs/conform" = "v0.1.0-alpha.30" +"forgejo:itiquette/gommitlint" = "0.9.3" "aqua:zricethezav/gitleaks" = "v8.30.0" "ubi:rvben/rumdl" = "v0.1.25" "aqua:koalaman/shellcheck" = "v0.11.0" diff --git a/README.md b/README.md index 2ae6bb8..3a8b128 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ Run on every project. Skip automatically if no relevant files found: | Recipe | Tool | Checks | Skips when | |--------|------|--------|------------| -| `lint-commits` | conform | Commit message format | On default branch or no new commits | +| `lint-version-control` | git | Working tree is clean and version controlled | Never (fails if dirty or outside a Git repo) | +| `lint-commits` | gommitlint | Commit message format | On default branch or no new commits | | `lint-secrets` | gitleaks | Secrets/credentials | Never (scans commits) | | `lint-yaml` | yamlfmt | YAML formatting | No .yml/.yaml files | | `lint-markdown` | rumdl | Markdown style | No .md files | @@ -438,6 +439,7 @@ devbase-check/ │ ├── secrets.sh │ ├── shell-fmt.sh │ ├── shell.sh +│ ├── version-control.sh │ ├── xml.sh │ └── yaml.sh ├── scripts/ diff --git a/examples/base-justfile b/examples/base-justfile index 26dd16b..e50fb80 100644 --- a/examples/base-justfile +++ b/examples/base-justfile @@ -57,7 +57,7 @@ setup-devtools: # Check required tools [group('setup')] check-tools: _ensure-devtools - @{{devtools_dir}}/scripts/check-tools.sh --check-devtools mise git just rumdl yamlfmt actionlint gitleaks shellcheck shfmt conform reuse hadolint + @{{devtools_dir}}/scripts/check-tools.sh --check-devtools mise git just rumdl yamlfmt actionlint gitleaks shellcheck shfmt gommitlint reuse hadolint # Install tools via mise [group('setup')] diff --git a/examples/java-justfile b/examples/java-justfile index 9021c07..33917e6 100644 --- a/examples/java-justfile +++ b/examples/java-justfile @@ -59,7 +59,7 @@ setup-devtools: # Check required tools [group('setup')] check-tools: _ensure-devtools - @{{devtools_dir}}/scripts/check-tools.sh --check-devtools mise git just java mvn rumdl yamlfmt actionlint gitleaks shellcheck shfmt conform reuse + @{{devtools_dir}}/scripts/check-tools.sh --check-devtools mise git just java mvn rumdl yamlfmt actionlint gitleaks shellcheck shfmt gommitlint reuse # Install tools via mise [group('setup')] diff --git a/examples/node-justfile b/examples/node-justfile index f9991bb..92037d9 100644 --- a/examples/node-justfile +++ b/examples/node-justfile @@ -57,7 +57,7 @@ setup-devtools: # Check required tools [group('setup')] check-tools: _ensure-devtools - @{{devtools_dir}}/scripts/check-tools.sh --check-devtools mise git just node npm rumdl yamlfmt actionlint gitleaks shellcheck shfmt conform reuse + @{{devtools_dir}}/scripts/check-tools.sh --check-devtools mise git just node npm rumdl yamlfmt actionlint gitleaks shellcheck shfmt gommitlint reuse # Install Node dependencies [group('setup')] diff --git a/justfile b/justfile index 1fa3aa9..4502a26 100644 --- a/justfile +++ b/justfile @@ -58,7 +58,12 @@ lint-base: [group('lint')] lint-all: lint-base -# Validate commit messages (conform) +# Validate version control +[group('lint')] +lint-version-control: + @{{lint}}/version-control.sh + +# Validate commit messages (gommitlint) [group('lint')] lint-commits: @{{lint}}/commits.sh diff --git a/linters/commits.sh b/linters/commits.sh index ff80f24..8ebc261 100755 --- a/linters/commits.sh +++ b/linters/commits.sh @@ -10,35 +10,66 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/../utils/colors.sh" source "${SCRIPT_DIR}/../utils/git-utils.sh" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + main() { - print_header "COMMIT HEALTH (CONFORM)" + print_header "COMMIT HEALTH (GOMMITLINT)" - local current_branch default_branch + local current_branch default_branch base_branch local_base remote_base current_branch=$(git branch --show-current) default_branch=$(get_default_branch) + local_base="$default_branch" + remote_base="origin/${default_branch}" + base_branch="$local_base" + + # Prefer local default branch first. Fall back to origin/ when local + # is missing or not in HEAD ancestry (avoids false ahead counts in diverged trees). + if branch_exists "$local_base"; then + if ! git merge-base --is-ancestor "$local_base" HEAD >/dev/null 2>&1 && branch_exists "$remote_base"; then + base_branch="$remote_base" + fi + elif branch_exists "$remote_base"; then + base_branch="$remote_base" + fi - # Skip if on the base branch itself (conform can't handle base..HEAD when they're the same) + # Skip if on the base branch itself (gommitlint can't handle base..HEAD when they're the same) if [[ "$current_branch" == "$default_branch" ]]; then print_info "On ${default_branch} - no commits to check against base branch" + emit_status "na" "n/a" return 0 fi - if ! has_commits_since "$default_branch"; then - print_info "No commits to check on ${current_branch} (compared to ${default_branch})" + if ! has_commits_since "$base_branch"; then + print_info "No commits to check on ${current_branch} (compared to ${base_branch})" + emit_status "na" "n/a" return 0 fi - if ! command -v conform >/dev/null 2>&1; then - print_warning "conform not found in PATH - skipping commit linting" + # Detect SHA-256 repo and select correct binary + # See: https://github.com/go-git/go-git/issues/706 + local gommitlint_cmd="gommitlint" + if git rev-parse --show-object-format 2>/dev/null | grep -q sha256; then + gommitlint_cmd="gommitlint-sha256" + fi + + if ! command -v "$gommitlint_cmd" >/dev/null 2>&1; then + print_warning "${gommitlint_cmd} not found in PATH - skipping commit linting" echo " Install: mise install" + emit_status "skip" "not in PATH" return 0 fi - if conform enforce --base-branch="${default_branch}" 2>/dev/null; then + if $gommitlint_cmd validate --base-branch="${base_branch}" 2>/dev/null; then print_success "Commit health check passed" + emit_status "pass" "ok" return 0 else print_error "Commit health check failed - check your commit messages" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/container.sh b/linters/container.sh index 1d39d62..82733e3 100755 --- a/linters/container.sh +++ b/linters/container.sh @@ -9,6 +9,12 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/../utils/colors.sh" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + find_containerfiles() { find . -type f \( -name "Containerfile" -o -name "Containerfile.*" -o -name "Dockerfile" -o -name "Dockerfile.*" \) -not -path "./.git/*" 2>/dev/null } @@ -21,12 +27,14 @@ main() { if [[ -z "$files" ]]; then print_info "No Containerfile/Dockerfile found to check" + emit_status "na" "n/a" return 0 fi if ! command -v hadolint >/dev/null 2>&1; then print_warning "hadolint not found in PATH - skipping container linting" echo " Install: mise install" + emit_status "skip" "not in PATH" return 0 fi @@ -40,9 +48,11 @@ main() { if [[ $failed -eq 0 ]]; then print_success "Container linting passed" + emit_status "pass" "ok" return 0 else print_error "Container linting failed" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/github-actions.sh b/linters/github-actions.sh index 04e4b99..36851b4 100755 --- a/linters/github-actions.sh +++ b/linters/github-actions.sh @@ -9,25 +9,35 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/../utils/colors.sh" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + main() { print_header "GITHUB ACTIONS LINTING (ACTIONLINT)" if [[ ! -d .github/workflows ]]; then print_info "No GitHub Actions workflows found to check" + emit_status "na" "n/a" return 0 fi if ! command -v actionlint >/dev/null 2>&1; then print_warning "actionlint not found in PATH - skipping GitHub Actions linting" echo " Install: mise install" + emit_status "skip" "not in PATH" return 0 fi if actionlint; then print_success "GitHub Actions linting passed" + emit_status "pass" "ok" return 0 else print_error "GitHub Actions linting failed" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/java/checkstyle.sh b/linters/java/checkstyle.sh index 9c9b432..c8c9d53 100755 --- a/linters/java/checkstyle.sh +++ b/linters/java/checkstyle.sh @@ -11,24 +11,34 @@ source "${SCRIPT_DIR}/../../utils/colors.sh" maven_opts=(--batch-mode --no-transfer-progress --errors -Dstyle.color=always) +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + main() { print_header "JAVA CHECKSTYLE" if [[ ! -f pom.xml ]]; then print_warning "No pom.xml found, skipping" + emit_status "skip" "skipped" return 0 fi if ! command -v mvn >/dev/null 2>&1; then print_error "mvn not found. Install with: mise install maven" + emit_status "fail" "failed" return 1 fi if mvn "${maven_opts[@]}" checkstyle:check; then print_success "Checkstyle passed" + emit_status "pass" "ok" return 0 else print_error "Checkstyle failed" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/java/format.sh b/linters/java/format.sh index dc493d5..36930ca 100755 --- a/linters/java/format.sh +++ b/linters/java/format.sh @@ -12,9 +12,16 @@ source "${SCRIPT_DIR}/../../utils/colors.sh" maven_opts=(--batch-mode --no-transfer-progress --errors -Dstyle.color=always) readonly ACTION="${1:-check}" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + check_maven() { if ! command -v mvn >/dev/null 2>&1; then print_error "mvn not found. Install with: mise install maven" + emit_status "fail" "failed" return 1 fi } @@ -27,9 +34,11 @@ check_format() { print_info "Checking Java formatting..." if mvn "${maven_opts[@]}" formatter:validate; then print_success "Java formatting check passed" + emit_status "pass" "ok" return 0 else print_error "Java formatting check failed - run 'just lint-java-fmt-fix' to fix" + emit_status "fail" "failed" return 1 fi } @@ -38,9 +47,11 @@ fix_format() { print_info "Formatting Java code..." if mvn "${maven_opts[@]}" formatter:format; then print_success "Java code formatted" + emit_status "pass" "ok" return 0 else print_error "Java formatting failed" + emit_status "fail" "failed" return 1 fi } @@ -50,6 +61,7 @@ main() { if ! has_pom; then print_warning "No pom.xml found, skipping" + emit_status "skip" "skipped" return 0 fi @@ -63,6 +75,7 @@ main() { *) print_error "Unknown action: $ACTION" printf "Usage: %s [check|fix]\n" "$0" + emit_status "fail" "failed" return 1 ;; esac diff --git a/linters/java/lint.sh b/linters/java/lint.sh index 5cc978f..0479695 100755 --- a/linters/java/lint.sh +++ b/linters/java/lint.sh @@ -11,22 +11,31 @@ source "${SCRIPT_DIR}/../../utils/colors.sh" maven_opts=(--batch-mode --no-transfer-progress --errors -Dstyle.color=always) +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + main() { print_header "JAVA LINTING (ALL)" if [[ ! -f pom.xml ]]; then print_warning "No pom.xml found, skipping" + emit_status "skip" "skipped" return 0 fi if ! command -v mvn >/dev/null 2>&1; then print_error "mvn not found. Install with: mise install maven" + emit_status "fail" "failed" return 1 fi print_info "Building project (skip tests)..." if ! mvn "${maven_opts[@]}" install -DskipTests; then print_error "Build failed" + emit_status "fail" "failed" return 1 fi @@ -38,9 +47,11 @@ main() { if [[ $failed -eq 0 ]]; then print_success "All Java linting passed" + emit_status "pass" "ok" return 0 else print_error "Some Java linting failed" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/java/pmd.sh b/linters/java/pmd.sh index 76d70b8..1719a4f 100755 --- a/linters/java/pmd.sh +++ b/linters/java/pmd.sh @@ -11,24 +11,34 @@ source "${SCRIPT_DIR}/../../utils/colors.sh" maven_opts=(--batch-mode --no-transfer-progress --errors -Dstyle.color=always) +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + main() { print_header "JAVA PMD" if [[ ! -f pom.xml ]]; then print_warning "No pom.xml found, skipping" + emit_status "skip" "skipped" return 0 fi if ! command -v mvn >/dev/null 2>&1; then print_error "mvn not found. Install with: mise install maven" + emit_status "fail" "failed" return 1 fi if mvn "${maven_opts[@]}" pmd:check; then print_success "PMD passed" + emit_status "pass" "ok" return 0 else print_error "PMD failed" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/java/spotbugs.sh b/linters/java/spotbugs.sh index 697a3d7..cecb8ed 100755 --- a/linters/java/spotbugs.sh +++ b/linters/java/spotbugs.sh @@ -14,16 +14,24 @@ maven_opts=(--batch-mode --no-transfer-progress --errors -Dstyle.color=always) # Default exclude file from devbase-check (excludes generated-sources) DEFAULT_EXCLUDE="${SCRIPT_DIR}/config/spotbugs-exclude.xml" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + main() { print_header "JAVA SPOTBUGS" if [[ ! -f pom.xml ]]; then print_warning "No pom.xml found, skipping" + emit_status "skip" "skipped" return 0 fi if ! command -v mvn >/dev/null 2>&1; then print_error "mvn not found. Install with: mise install maven" + emit_status "fail" "failed" return 1 fi @@ -39,9 +47,11 @@ main() { if mvn "${maven_opts[@]}" ${exclude_opt[@]+"${exclude_opt[@]}"} spotbugs:check; then print_success "SpotBugs passed" + emit_status "pass" "ok" return 0 else print_error "SpotBugs failed" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/java/test.sh b/linters/java/test.sh index 7af36c3..8e72002 100755 --- a/linters/java/test.sh +++ b/linters/java/test.sh @@ -11,9 +11,16 @@ source "${SCRIPT_DIR}/../../utils/colors.sh" maven_opts=(--batch-mode --no-transfer-progress --errors -Dstyle.color=always) +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + check_maven() { if ! command -v mvn >/dev/null 2>&1; then print_error "mvn not found. Install with: mise install maven" + emit_status "fail" "failed" return 1 fi } @@ -27,6 +34,7 @@ main() { if ! has_pom; then print_warning "No pom.xml found, skipping" + emit_status "skip" "skipped" return 0 fi @@ -37,9 +45,11 @@ main() { print_info "Running tests..." if mvn "${maven_opts[@]}" clean verify; then print_success "Java tests passed" + emit_status "pass" "ok" return 0 else print_error "Java tests failed" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/license.sh b/linters/license.sh index e839a48..c12edd0 100755 --- a/linters/license.sh +++ b/linters/license.sh @@ -9,20 +9,29 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/../utils/colors.sh" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + main() { print_header "LICENSE COMPLIANCE (REUSE)" if ! command -v reuse >/dev/null 2>&1; then print_warning "reuse not found in PATH - skipping license compliance check" echo " Install: mise install" + emit_status "skip" "not in PATH" return 0 fi if reuse lint; then print_success "License compliance check passed" + emit_status "pass" "ok" return 0 else print_error "License compliance check failed" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/markdown.sh b/linters/markdown.sh index 75ff6d0..9878a8c 100755 --- a/linters/markdown.sh +++ b/linters/markdown.sh @@ -13,6 +13,12 @@ readonly ACTION="${1:-check}" shift || true readonly DISABLE="${1:-MD013}" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + readonly EXCLUDE=".github-shared,node_modules,vendor,target,CHANGELOG.md" find_markdown_files() { @@ -31,9 +37,11 @@ check_markdown() { [[ -n "$DISABLE" ]] && args+=(--disable "$DISABLE") if rumdl "${args[@]}"; then print_success "Markdown linting passed" + emit_status "pass" "ok" return 0 else print_error "Markdown linting failed - run 'just lint-markdown-fix' to fix" + emit_status "fail" "failed" return 1 fi } @@ -43,9 +51,11 @@ fix_markdown() { [[ -n "$DISABLE" ]] && args+=(--disable "$DISABLE") if rumdl "${args[@]}"; then print_success "Markdown files fixed" + emit_status "pass" "ok" return 0 else print_error "Failed to fix markdown files" + emit_status "fail" "failed" return 1 fi } @@ -58,12 +68,14 @@ main() { if [[ -z "$files" ]]; then print_info "No Markdown files found to check" + emit_status "na" "n/a" return 0 fi if ! command -v rumdl >/dev/null 2>&1; then print_warning "rumdl not found in PATH - skipping markdown linting" echo " Install: mise install" + emit_status "skip" "not in PATH" return 0 fi @@ -73,6 +85,7 @@ main() { *) print_error "Unknown action: $ACTION" printf "Usage: %s [check|fix]\n" "$0" + emit_status "fail" "failed" return 1 ;; esac diff --git a/linters/node/eslint.sh b/linters/node/eslint.sh index 1645b2b..67fdda8 100755 --- a/linters/node/eslint.sh +++ b/linters/node/eslint.sh @@ -9,22 +9,31 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/../../utils/colors.sh" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + main() { print_header "NODE ESLINT (JS/TS)" if ! command -v npx >/dev/null 2>&1; then print_error "npx not found. Install Node.js and npm" + emit_status "fail" "failed" return 1 fi # Check if project has ESLint configured if [[ ! -f "package.json" ]]; then print_warning "No package.json found. Skipping ESLint" + emit_status "skip" "skipped" return 0 fi if ! grep -q "eslint" package.json 2>/dev/null; then print_warning "ESLint not configured in package.json. Skipping" + emit_status "skip" "skipped" return 0 fi @@ -38,9 +47,11 @@ main() { if [[ $? -eq 0 ]]; then print_success "ESLint check passed" + emit_status "pass" "ok" return 0 else print_error "ESLint check failed - run 'npm run lint -- --fix' or 'npx eslint . --fix' to fix" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/node/format.sh b/linters/node/format.sh index 20e91bb..ea88f2c 100755 --- a/linters/node/format.sh +++ b/linters/node/format.sh @@ -11,13 +11,21 @@ source "${SCRIPT_DIR}/../../utils/colors.sh" readonly ACTION="${1:-check}" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + check_prettier() { npx prettier --check . if [[ $? -eq 0 ]]; then print_success "Prettier check passed" + emit_status "pass" "ok" return 0 else print_error "Prettier check failed - run 'just lint-node-format-fix' to fix" + emit_status "fail" "failed" return 1 fi } @@ -26,9 +34,11 @@ fix_prettier() { npx prettier --write . if [[ $? -eq 0 ]]; then print_success "Prettier formatting applied" + emit_status "pass" "ok" return 0 else print_error "Prettier formatting failed" + emit_status "fail" "failed" return 1 fi } @@ -38,17 +48,20 @@ main() { if ! command -v npx >/dev/null 2>&1; then print_error "npx not found. Install Node.js and npm" + emit_status "fail" "failed" return 1 fi # Check if project has Prettier configured if [[ ! -f "package.json" ]]; then print_warning "No package.json found. Skipping Prettier" + emit_status "skip" "skipped" return 0 fi if ! grep -q "prettier" package.json 2>/dev/null; then print_warning "Prettier not configured in package.json. Skipping" + emit_status "skip" "skipped" return 0 fi @@ -58,6 +71,7 @@ main() { *) print_error "Unknown action: $ACTION" printf "Usage: %s [check|fix]\n" "$0" + emit_status "fail" "failed" return 1 ;; esac diff --git a/linters/node/lint.sh b/linters/node/lint.sh index 3b41de1..ce0bd24 100755 --- a/linters/node/lint.sh +++ b/linters/node/lint.sh @@ -9,6 +9,12 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/../../utils/colors.sh" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + print_header "NODE LINTING (ALL)" has_errors=false @@ -39,8 +45,10 @@ fi if [ "$has_errors" = true ]; then print_error "Node linting failed" + emit_status "fail" "failed" exit 1 else print_success "All Node linting passed" + emit_status "pass" "ok" exit 0 fi diff --git a/linters/node/types.sh b/linters/node/types.sh index a3b7b32..c3047f5 100755 --- a/linters/node/types.sh +++ b/linters/node/types.sh @@ -9,22 +9,31 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/../../utils/colors.sh" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + main() { print_header "NODE TYPE CHECKING (TSC)" if ! command -v npx >/dev/null 2>&1; then print_error "npx not found. Install Node.js and npm" + emit_status "fail" "failed" return 1 fi # Check if project has TypeScript configured if [[ ! -f "tsconfig.json" ]] && [[ ! -f "package.json" ]]; then print_warning "No tsconfig.json or package.json found. Skipping type checking" + emit_status "skip" "skipped" return 0 fi if [[ -f "package.json" ]] && ! grep -q "typescript" package.json 2>/dev/null; then print_warning "TypeScript not configured in package.json. Skipping" + emit_status "skip" "skipped" return 0 fi @@ -44,9 +53,11 @@ main() { if [[ $? -eq 0 ]]; then print_success "Type checking passed" + emit_status "pass" "ok" return 0 else print_error "Type checking failed - fix type errors" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/secrets.sh b/linters/secrets.sh index 90a8ad0..0811526 100755 --- a/linters/secrets.sh +++ b/linters/secrets.sh @@ -10,12 +10,19 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/../utils/colors.sh" source "${SCRIPT_DIR}/../utils/git-utils.sh" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + main() { print_header "SECRET SCANNING (GITLEAKS)" if ! command -v gitleaks >/dev/null 2>&1; then print_warning "gitleaks not found in PATH - skipping secret scanning" echo " Install: mise install" + emit_status "skip" "not in PATH" return 0 fi @@ -40,9 +47,11 @@ main() { if [[ $gitleaks_result -eq 0 ]]; then print_success "No secrets found" + emit_status "pass" "ok" return 0 else print_error "Secret scanning failed - secrets may be present!" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/shell-fmt.sh b/linters/shell-fmt.sh index 73b36e5..7dd0931 100755 --- a/linters/shell-fmt.sh +++ b/linters/shell-fmt.sh @@ -11,6 +11,12 @@ source "${SCRIPT_DIR}/../utils/colors.sh" readonly MODE="${1:-check}" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + find_shell_scripts() { find . -type f \( -name "*.sh" -o -name "*.bash" \) \ -not -path "./.git/*" \ @@ -25,9 +31,11 @@ check_format() { local scripts="$1" if echo "$scripts" | xargs -r shfmt -i 2 -d; then print_success "Shell script formatting check passed" + emit_status "pass" "ok" return 0 else print_error "Shell script formatting failed - run 'just lint-shell-fmt-fix' to fix" + emit_status "fail" "failed" return 1 fi } @@ -36,9 +44,11 @@ fix_format() { local scripts="$1" if echo "$scripts" | xargs -r shfmt -i 2 -w; then print_success "Shell scripts formatted" + emit_status "pass" "ok" return 0 else print_error "Shell script formatting failed" + emit_status "fail" "failed" return 1 fi } @@ -51,12 +61,14 @@ main() { if [[ -z "$scripts" ]]; then print_info "No shell scripts found to format" + emit_status "na" "n/a" return 0 fi if ! command -v shfmt >/dev/null 2>&1; then print_warning "shfmt not found in PATH - skipping shell formatting" echo " Install: mise install" + emit_status "skip" "not in PATH" return 0 fi @@ -66,6 +78,7 @@ main() { *) print_error "Unknown mode: $MODE" printf "Usage: %s [check|fix]\n" "$0" + emit_status "fail" "failed" return 1 ;; esac diff --git a/linters/shell.sh b/linters/shell.sh index 4e6f425..6bf9e33 100755 --- a/linters/shell.sh +++ b/linters/shell.sh @@ -9,6 +9,12 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/../utils/colors.sh" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + find_shell_scripts() { find . -type f \( -name "*.sh" -o -name "*.bash" \) \ -not -path "./.git/*" \ @@ -35,12 +41,14 @@ main() { if [[ -z "$scripts" ]]; then print_info "No shell scripts found to check" + emit_status "na" "n/a" return 0 fi if ! command -v shellcheck >/dev/null 2>&1; then print_warning "shellcheck not found in PATH - skipping shell linting" echo " Install: mise install" + emit_status "skip" "not in PATH" return 0 fi @@ -61,9 +69,11 @@ main() { if [[ $failed -eq 0 ]]; then print_success "Shell script linting passed" + emit_status "pass" "ok" return 0 else print_error "Shell script linting failed" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/version-control.sh b/linters/version-control.sh new file mode 100755 index 0000000..f903517 --- /dev/null +++ b/linters/version-control.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2025 Digg - Agency for Digital Government +# +# SPDX-License-Identifier: MIT + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../utils/colors.sh" + +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + +main() { + print_header "WORKING TREE" + + if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + print_error "Not a Git repository - cannot verify version control state" + emit_status "fail" "failed" + return 1 + fi + + if [[ -z "$(git status --porcelain)" ]]; then + print_success "All changes are under version control" + emit_status "pass" "ok" + return 0 + else + print_error "Some changes are not under version control! + + This can happen if + + 1. You forgot to version control your changes + 2. A linter automatically fixed a problem or reformatted the code. + + Please accept or discard any outstanding changes and try again." + emit_status "fail" "failed" + return 1 + fi +} + +main diff --git a/linters/xml.sh b/linters/xml.sh index 5f00b7e..b48a900 100755 --- a/linters/xml.sh +++ b/linters/xml.sh @@ -9,6 +9,12 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/../utils/colors.sh" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + find_xml_files() { find . -type f -name "*.xml" -not -path "./.git/*" -not -path "./target/*" -not -path "./.idea/*" -not -path "./node_modules/*" 2>/dev/null } @@ -21,6 +27,7 @@ main() { if [[ -z "$files" ]]; then print_info "No XML files found to check" + emit_status "na" "n/a" return 0 fi @@ -29,6 +36,7 @@ main() { echo " Install: Ubuntu/Debian: sudo apt install libxml2-utils" echo " Fedora/RHEL: sudo dnf install libxml2" echo " macOS: brew install libxml2" + emit_status "skip" "not in PATH" return 0 fi @@ -44,9 +52,11 @@ main() { if [[ $failed -eq 0 ]]; then print_success "XML linting passed ($count files)" + emit_status "pass" "ok" return 0 else print_error "XML linting failed" + emit_status "fail" "failed" return 1 fi } diff --git a/linters/yaml.sh b/linters/yaml.sh index 9b79699..d2039ea 100755 --- a/linters/yaml.sh +++ b/linters/yaml.sh @@ -11,6 +11,12 @@ source "${SCRIPT_DIR}/../utils/colors.sh" readonly ACTION="${1:-check}" +emit_status() { + [[ "${DEVBASE_CHECK_MARKERS:-0}" == "1" ]] || return 0 + printf "DEVBASE_CHECK_STATUS=%s\n" "$1" + [[ -n "${2:-}" ]] && printf "DEVBASE_CHECK_DETAILS=%s\n" "$2" +} + # Default config with standard exclusions readonly DEFAULT_CONFIG="${SCRIPT_DIR}/config/.yamlfmt" @@ -23,9 +29,30 @@ find_yaml_files() { 2>/dev/null } +has_local_config() { + local config_files=( + ".yamlfmt" + ".yamlfmt.yml" + ".yamlfmt.yaml" + "yamlfmt.yml" + "yamlfmt.yaml" + ) + + for file in "${config_files[@]}"; do + [[ -f "$file" ]] && return 0 + done + + return 1 +} + get_config_flag() { - # If project has its own config, use default behavior; otherwise use our default - if [[ ! -f ".yamlfmt" && ! -f "yamlfmt.yml" && ! -f "yamlfmt.yaml" && -f "${DEFAULT_CONFIG}" ]]; then + # If project has its own config, do nothing + if has_local_config; then + return 0 + fi + + # Otherwise, use default config if it exists + if [[ -f "${DEFAULT_CONFIG}" ]]; then printf "%s" "-conf ${DEFAULT_CONFIG}" fi } @@ -36,9 +63,11 @@ check_yaml() { # shellcheck disable=SC2086 if yamlfmt -lint $conf_flag .; then print_success "YAML linting passed" + emit_status "pass" "ok" return 0 else print_error "YAML linting failed - run 'just lint-yaml-fix' to fix" + emit_status "fail" "failed" return 1 fi } @@ -49,9 +78,11 @@ fix_yaml() { # shellcheck disable=SC2086 if yamlfmt $conf_flag .; then print_success "YAML files formatted" + emit_status "pass" "ok" return 0 else print_error "Failed to format YAML files" + emit_status "fail" "failed" return 1 fi } @@ -64,12 +95,14 @@ main() { if [[ -z "$files" ]]; then print_info "No YAML files found to check" + emit_status "na" "n/a" return 0 fi if ! command -v yamlfmt >/dev/null 2>&1; then print_warning "yamlfmt not found in PATH - skipping YAML linting" echo " Install: mise install" + emit_status "skip" "not in PATH" return 0 fi @@ -79,6 +112,7 @@ main() { *) print_error "Unknown action: $ACTION" printf "Usage: %s [check|fix]\n" "$0" + emit_status "fail" "failed" return 1 ;; esac diff --git a/renovate.json b/renovate.json index c76a327..7838365 100644 --- a/renovate.json +++ b/renovate.json @@ -33,7 +33,7 @@ ], "matchPackageNames": [ "aqua:zricethezav/gitleaks", - "aqua:siderolabs/conform", + "forgejo:itiquette/gommitlint", "aqua:rhysd/actionlint", "pipx:reuse" ], diff --git a/scripts/verify.sh b/scripts/verify.sh index 7e289f9..40d0e57 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -17,7 +17,8 @@ source "${SCRIPT_DIR}/../summary/common.sh" # Base linters LINTERS=( - "Commits|conform|just lint-commits" + "Working Tree|git|just lint-version-control" + "Commits|gommitlint|just lint-commits" "Secrets|gitleaks|just lint-secrets" "YAML|yamlfmt|just lint-yaml" "Markdown|rumdl|just lint-markdown" @@ -32,6 +33,25 @@ LINTERS=( declare -A RESULTS declare -A OUTPUTS +extract_status_marker() { + local output="$1" + local marker + marker=$(grep -oE '^DEVBASE_CHECK_STATUS=(pass|fail|skip|na|n/a|disabled)$' <<<"$output" | tail -1 || true) + printf "%s" "${marker#DEVBASE_CHECK_STATUS=}" +} + +extract_details_marker() { + local output="$1" + local marker + marker=$(grep -oE '^DEVBASE_CHECK_DETAILS=.*$' <<<"$output" | tail -1 || true) + printf "%s" "${marker#DEVBASE_CHECK_DETAILS=}" +} + +strip_status_markers() { + local output="$1" + grep -vE '^DEVBASE_CHECK_(STATUS|DETAILS)=' <<<"$output" || true +} + detect_language_linters() { local recipes recipes=$(just --list 2>&1 || true) @@ -65,16 +85,38 @@ run_linters() { for linter_def in "${LINTERS[@]}"; do IFS='|' read -r check tool cmd <<<"$linter_def" - local output exit_code=0 - output=$(eval "$cmd" 2>&1) || exit_code=$? - echo "$output" + local raw_output output exit_code=0 + raw_output=$(DEVBASE_CHECK_MARKERS=1 eval "$cmd" 2>&1) || exit_code=$? + + output=$(strip_status_markers "$raw_output") + [[ -n "$output" ]] && echo "$output" # Store output for summary module OUTPUTS["$check"]="$output" # Parse status from output local status details - if [[ $exit_code -eq 0 ]]; then + local status_marker details_marker + status_marker=$(extract_status_marker "$raw_output") + details_marker=$(extract_details_marker "$raw_output") + + if [[ -n "$status_marker" ]]; then + case "$status_marker" in + na) status="n/a" ;; + *) status="$status_marker" ;; + esac + + case "$status" in + fail) + details="${details_marker:-failed}" + ((failed++)) + ;; + skip) details="${details_marker:-skipped}" ;; + n/a) details="${details_marker:-n/a}" ;; + disabled) details="" ;; + *) details="${details_marker:-ok}" ;; + esac + elif [[ $exit_code -eq 0 ]]; then if [[ -z "${output// /}" ]]; then # Empty output = linter disabled, skip entirely status="disabled" @@ -82,7 +124,7 @@ run_linters() { elif grep -q "not found in PATH" <<<"$output"; then status="skip" details="not in PATH" - elif grep -qiE "Skipping|Skip" <<<"$output"; then + elif grep -qiE "No .* found, skipping|No pom.xml found, skipping" <<<"$output"; then status="skip" details="skipped" elif grep -qE "No .* (files? found|to check)|no commits to check" <<<"$output"; then diff --git a/tests/linters-commits.bats b/tests/linters-commits.bats index 98d16c2..d93ed60 100644 --- a/tests/linters-commits.bats +++ b/tests/linters-commits.bats @@ -15,12 +15,13 @@ load "${BATS_TEST_DIRNAME}/test_helper.bash" setup() { common_setup export LINTERS_DIR="${DEVTOOLS_ROOT}/linters" + export DEVBASE_CHECK_MARKERS=1 cd "$TEST_DIR" init_git_repo } teardown() { - unstub conform 2>/dev/null || true + unstub gommitlint 2>/dev/null || true common_teardown } @@ -30,4 +31,5 @@ teardown() { [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'" assert_success assert_output --partial "no commits to check" + assert_output --partial "DEVBASE_CHECK_STATUS=na" } diff --git a/tests/linters-java.bats b/tests/linters-java.bats index 22d1d3b..68b0e3c 100644 --- a/tests/linters-java.bats +++ b/tests/linters-java.bats @@ -15,6 +15,7 @@ load "${BATS_TEST_DIRNAME}/test_helper.bash" setup() { common_setup export JAVA_LINTERS="${DEVTOOLS_ROOT}/linters/java" + export DEVBASE_CHECK_MARKERS=1 cd "$TEST_DIR" } @@ -75,6 +76,37 @@ EOF assert_success assert_output --partial "No pom.xml" + assert_output --partial "DEVBASE_CHECK_STATUS=skip" +} + +@test "spotbugs.sh reports pass marker when mvn succeeds" { + cat > pom.xml << 'EOF' + + 4.0.0 + +EOF + stub_repeated mvn "true" + + run "$JAVA_LINTERS/spotbugs.sh" + + assert_success + assert_output --partial "SpotBugs passed" + assert_output --partial "DEVBASE_CHECK_STATUS=pass" +} + +@test "spotbugs.sh reports fail marker when mvn fails" { + cat > pom.xml << 'EOF' + + 4.0.0 + +EOF + stub_repeated mvn "exit 1" + + run --separate-stderr "$JAVA_LINTERS/spotbugs.sh" + + assert_failure + [[ "$stderr" == *"SpotBugs failed"* ]] || [[ "$output" == *"SpotBugs failed"* ]] + assert_output --partial "DEVBASE_CHECK_STATUS=fail" } @test "format.sh skips when no pom.xml present" { diff --git a/tests/linters-markdown.bats b/tests/linters-markdown.bats index 2bc213a..19ce465 100644 --- a/tests/linters-markdown.bats +++ b/tests/linters-markdown.bats @@ -15,10 +15,12 @@ load "${BATS_TEST_DIRNAME}/test_helper.bash" setup() { common_setup export LINTERS_DIR="${DEVTOOLS_ROOT}/linters" + export DEVBASE_CHECK_MARKERS=1 cd "$TEST_DIR" } teardown() { + unstub rumdl 2>/dev/null || true common_teardown } @@ -33,6 +35,7 @@ EOF [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'" assert_success assert_output --partial "passed" + assert_output --partial "DEVBASE_CHECK_STATUS=pass" } @test "markdown.sh fix runs rumdl with --fix" { @@ -46,4 +49,17 @@ EOF [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'" assert_success assert_output --partial "fixed" + assert_output --partial "DEVBASE_CHECK_STATUS=pass" +} + +@test "markdown.sh check reports fail marker when rumdl fails" { + cat > test.md << 'EOF' +# Test +EOF + stub_repeated rumdl "exit 1" + + run --separate-stderr "$LINTERS_DIR/markdown.sh" check + + assert_failure + assert_output --partial "DEVBASE_CHECK_STATUS=fail" } diff --git a/tests/linters-node.bats b/tests/linters-node.bats index 806d23f..42bb733 100644 --- a/tests/linters-node.bats +++ b/tests/linters-node.bats @@ -15,6 +15,7 @@ load "${BATS_TEST_DIRNAME}/test_helper.bash" setup() { common_setup export NODE_LINTERS="${DEVTOOLS_ROOT}/linters/node" + export DEVBASE_CHECK_MARKERS=1 cd "$TEST_DIR" } @@ -28,6 +29,7 @@ teardown() { assert_success assert_output --partial "package.json" + assert_output --partial "DEVBASE_CHECK_STATUS=skip" } @test "eslint.sh skips when eslint not in package.json" { @@ -42,6 +44,7 @@ EOF assert_success assert_output --partial "ESLint" + assert_output --partial "DEVBASE_CHECK_STATUS=skip" } @test "eslint.sh runs npx eslint when configured" { @@ -58,6 +61,24 @@ EOF run "$NODE_LINTERS/eslint.sh" assert_success + assert_output --partial "DEVBASE_CHECK_STATUS=pass" +} + +@test "eslint.sh reports fail marker when eslint fails" { + cat > package.json << 'EOF' +{ + "name": "test", + "devDependencies": { + "eslint": "^8.0.0" + } +} +EOF + stub_repeated npx "exit 1" + + run --separate-stderr "$NODE_LINTERS/eslint.sh" + + assert_failure + assert_output --partial "DEVBASE_CHECK_STATUS=fail" } @test "format.sh skips when no package.json present" { diff --git a/tests/linters-secrets.bats b/tests/linters-secrets.bats index 068515d..516ff2c 100644 --- a/tests/linters-secrets.bats +++ b/tests/linters-secrets.bats @@ -15,6 +15,7 @@ load "${BATS_TEST_DIRNAME}/test_helper.bash" setup() { common_setup export LINTERS_DIR="${DEVTOOLS_ROOT}/linters" + export DEVBASE_CHECK_MARKERS=1 cd "$TEST_DIR" init_git_repo } @@ -32,6 +33,7 @@ teardown() { [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'" assert_success assert_output --partial "No secrets" + assert_output --partial "DEVBASE_CHECK_STATUS=pass" } @test "secrets.sh fails when secrets detected" { @@ -41,4 +43,5 @@ teardown() { [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'" assert_failure + assert_output --partial "DEVBASE_CHECK_STATUS=fail" } diff --git a/tests/linters-version-control.bats b/tests/linters-version-control.bats new file mode 100644 index 0000000..64ac9ca --- /dev/null +++ b/tests/linters-version-control.bats @@ -0,0 +1,64 @@ +#!/usr/bin/env bats + +# SPDX-FileCopyrightText: 2025 Digg - Agency for Digital Government +# +# SPDX-License-Identifier: MIT + +bats_require_minimum_version 1.13.0 + +load "${BATS_TEST_DIRNAME}/libs/bats-support/load.bash" +load "${BATS_TEST_DIRNAME}/libs/bats-assert/load.bash" +load "${BATS_TEST_DIRNAME}/libs/bats-file/load.bash" +load "${BATS_TEST_DIRNAME}/libs/bats-mock/stub.bash" +load "${BATS_TEST_DIRNAME}/test_helper.bash" + +setup() { + common_setup + export LINTERS_DIR="${DEVTOOLS_ROOT}/linters" + export DEVBASE_CHECK_MARKERS=1 + cd "$TEST_DIR" + init_git_repo +} + +teardown() { + common_teardown +} + +@test "version-control.sh accepts clean working directory" { + run --separate-stderr "$LINTERS_DIR/version-control.sh" + + [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'" + assert_success + assert_output --partial "All changes are under version control" + assert_output --partial "DEVBASE_CHECK_STATUS=pass" +} + +@test "version-control.sh rejects unversioned file" { + touch dummy-file + run "$LINTERS_DIR/version-control.sh" + + [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'" + assert_failure + assert_output --partial "DEVBASE_CHECK_STATUS=fail" + assert_output --partial "Some changes are not under version control! + + This can happen if + + 1. You forgot to version control your changes + 2. A linter automatically fixed a problem or reformatted the code. + + Please accept or discard any outstanding changes and try again." +} + +@test "version-control.sh fails outside git repository" { + nonrepo_dir=$(mktemp -d) + + run bash -c "cd '$nonrepo_dir' && '$LINTERS_DIR/version-control.sh'" + + [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'" + assert_failure + assert_output --partial "DEVBASE_CHECK_STATUS=fail" + assert_output --partial "Not a Git repository - cannot verify version control state" + + rm -rf "$nonrepo_dir" +} diff --git a/tests/linters-yaml.bats b/tests/linters-yaml.bats index bdfcf98..85b064a 100644 --- a/tests/linters-yaml.bats +++ b/tests/linters-yaml.bats @@ -15,6 +15,7 @@ load "${BATS_TEST_DIRNAME}/test_helper.bash" setup() { common_setup export LINTERS_DIR="${DEVTOOLS_ROOT}/linters" + export DEVBASE_CHECK_MARKERS=1 cd "$TEST_DIR" } @@ -34,6 +35,7 @@ EOF [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'" assert_success assert_output --partial "passed" + assert_output --partial "DEVBASE_CHECK_STATUS=pass" } @test "yaml.sh check fails when yamlfmt finds issues" { @@ -46,6 +48,7 @@ EOF [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'" assert_failure + assert_output --partial "DEVBASE_CHECK_STATUS=fail" [[ "$stderr" == *"failed"* ]] || [[ "$output" == *"failed"* ]] } @@ -74,3 +77,64 @@ EOF assert_failure [[ "$stderr" == *"Unknown action"* ]] || [[ "$output" == *"Unknown action"* ]] } + +@test "yaml.sh uses project config for all supported local config variants" { + cat > test.yaml << 'EOF' +key: value +EOF + + mkdir -p "${TEST_DIR}/bin" + cat > "${TEST_DIR}/bin/yamlfmt" <<'EOF' +#!/usr/bin/env bash +printf "%s\n" "$*" > "${TEST_DIR}/yamlfmt.args" +exit 0 +EOF + chmod +x "${TEST_DIR}/bin/yamlfmt" + export PATH="${TEST_DIR}/bin:${PATH}" + + local configs=( + ".yamlfmt" + ".yamlfmt.yml" + ".yamlfmt.yaml" + "yamlfmt.yml" + "yamlfmt.yaml" + ) + + for cfg in "${configs[@]}"; do + rm -f .yamlfmt .yamlfmt.yml .yamlfmt.yaml yamlfmt.yml yamlfmt.yaml + : > "${TEST_DIR}/yamlfmt.args" + touch "$cfg" + + run --separate-stderr "$LINTERS_DIR/yaml.sh" check + + [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "cfg:${cfg} o:'${output}' e:'${stderr}'" + assert_success + args=$(<"${TEST_DIR}/yamlfmt.args") + [[ "$args" != *"-conf"* ]] + done +} + +@test "yaml.sh uses default config when no local config exists" { + cat > test.yaml << 'EOF' +key: value +EOF + + mkdir -p "${TEST_DIR}/bin" + cat > "${TEST_DIR}/bin/yamlfmt" <<'EOF' +#!/usr/bin/env bash +printf "%s\n" "$*" > "${TEST_DIR}/yamlfmt.args" +exit 0 +EOF + chmod +x "${TEST_DIR}/bin/yamlfmt" + export PATH="${TEST_DIR}/bin:${PATH}" + + rm -f .yamlfmt .yamlfmt.yml .yamlfmt.yaml yamlfmt.yml yamlfmt.yaml + + run --separate-stderr "$LINTERS_DIR/yaml.sh" check + + [ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'" + assert_success + args=$(<"${TEST_DIR}/yamlfmt.args") + [[ "$args" == *"-conf"* ]] + [[ "$args" == *"/linters/config/.yamlfmt"* ]] +} diff --git a/tests/verify.bats b/tests/verify.bats index 9cc7afc..aaa45b3 100644 --- a/tests/verify.bats +++ b/tests/verify.bats @@ -160,3 +160,75 @@ EOF assert_output --partial "Linting Results" assert_output --partial "| Linter | Tool | Status | Details |" } + +@test "verify.sh honors explicit pass marker even when output contains Skipping" { + cat > justfile << 'EOF' +lint-version-control: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-commits: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-secrets: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-yaml: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-markdown: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-shell: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-shell-fmt: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-actions: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-license: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-container: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-xml: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-java-spotbugs: + @printf "[INFO] Skipping com.github.spotbugs:spotbugs-maven-plugin report goal\n" + @printf "DEVBASE_CHECK_STATUS=pass\n" +EOF + + run "$SCRIPT_DIR/verify.sh" + + assert_success + assert_output --partial "Java SpotBugs" + refute_output --partial "1 skipped" +} + +@test "verify.sh fails when explicit fail marker is reported" { + cat > justfile << 'EOF' +lint-version-control: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-commits: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-secrets: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-yaml: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-markdown: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-shell: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-shell-fmt: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-actions: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-license: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-container: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-xml: + @printf "DEVBASE_CHECK_STATUS=pass\n" +lint-node-eslint: + @printf "DEVBASE_CHECK_STATUS=fail\n" + @printf "DEVBASE_CHECK_DETAILS=failed\n" +EOF + + run "$SCRIPT_DIR/verify.sh" + + assert_failure + assert_output --partial "Node ESLint" + assert_output --partial "1 failed" +} diff --git a/utils/git-utils.sh b/utils/git-utils.sh index edd55d8..1356f6a 100755 --- a/utils/git-utils.sh +++ b/utils/git-utils.sh @@ -59,6 +59,17 @@ get_default_branch() { # Returns: 0 if exists, 1 if not branch_exists() { local branch="$1" + + if [[ "$branch" == refs/* ]]; then + git show-ref --verify --quiet "$branch" 2>/dev/null + return $? + fi + + if [[ "$branch" == origin/* ]]; then + git show-ref --verify --quiet "refs/remotes/$branch" 2>/dev/null + return $? + fi + git show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null || git show-ref --verify --quiet "refs/remotes/origin/$branch" 2>/dev/null }