From 569580e795a165b2c7b8fb4d1bbcae3230cf54c9 Mon Sep 17 00:00:00 2001 From: Josef Andersson Date: Wed, 4 Mar 2026 20:18:07 +0100 Subject: [PATCH 1/7] fix: use reuseable-ci 2.6.1 Signed-off-by: Josef Andersson --- .github/workflows/openssf-scorecard.yml | 2 +- .github/workflows/pull-request-workflow.yml | 2 +- .github/workflows/release-workflow.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 From 82e94c9145aca15a6f99fa2eb89ce2ef044909f7 Mon Sep 17 00:00:00 2001 From: Josef Andersson Date: Wed, 4 Mar 2026 20:18:07 +0100 Subject: [PATCH 2/7] fix(yamlfmt): check all local config file variants yamlfmt script missed some valid file names when checking current dir for local config files This adds all possible files names mentioned in the docs: https://github.com/google/yamlfmt/blob/main/docs/config-file.md Signed-off-by: Asser Hakala Signed-off-by: Josef Andersson --- linters/yaml.sh | 25 +++++++++++++++-- tests/linters-yaml.bats | 61 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/linters/yaml.sh b/linters/yaml.sh index 9b79699..02f28c0 100755 --- a/linters/yaml.sh +++ b/linters/yaml.sh @@ -23,9 +23,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 } diff --git a/tests/linters-yaml.bats b/tests/linters-yaml.bats index bdfcf98..fd2212d 100644 --- a/tests/linters-yaml.bats +++ b/tests/linters-yaml.bats @@ -74,3 +74,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"* ]] +} From f92943663eca5505238ecf3584fc155c3d739ee5 Mon Sep 17 00:00:00 2001 From: Josef Andersson Date: Wed, 4 Mar 2026 20:18:07 +0100 Subject: [PATCH 3/7] fix(commits): replace conform with gommitlint Signed-off-by: Josef Andersson --- .conform.yaml | 19 --- .gommitlint.yaml | 8 ++ .mise.toml | 2 +- README.md | 2 +- examples/base-justfile | 2 +- examples/java-justfile | 2 +- examples/node-justfile | 2 +- justfile | 2 +- linters/commits.sh | 67 ++++++----- renovate.json | 2 +- scripts/verify.sh | 230 ++++++++++++++++++------------------- tests/linters-commits.bats | 2 +- 12 files changed, 168 insertions(+), 172 deletions(-) delete mode 100644 .conform.yaml create mode 100644 .gommitlint.yaml 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/.gommitlint.yaml b/.gommitlint.yaml new file mode 100644 index 0000000..69600ad --- /dev/null +++ b/.gommitlint.yaml @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2025 Digg - Agency for Digital Government +# +# SPDX-License-Identifier: CC0-1.0 + +gommitlint: + crypto_signature: + required: true + require_gpg: true 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..12d9f67 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ 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-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 | 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..803ccc0 100644 --- a/justfile +++ b/justfile @@ -58,7 +58,7 @@ lint-base: [group('lint')] lint-all: lint-base -# Validate commit messages (conform) +# Validate commit messages (gommitlint) [group('lint')] lint-commits: @{{lint}}/commits.sh diff --git a/linters/commits.sh b/linters/commits.sh index ff80f24..6e44d47 100755 --- a/linters/commits.sh +++ b/linters/commits.sh @@ -11,36 +11,43 @@ source "${SCRIPT_DIR}/../utils/colors.sh" source "${SCRIPT_DIR}/../utils/git-utils.sh" main() { - print_header "COMMIT HEALTH (CONFORM)" - - local current_branch default_branch - current_branch=$(git branch --show-current) - default_branch=$(get_default_branch) - - # Skip if on the base branch itself (conform 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" - return 0 - fi - - if ! has_commits_since "$default_branch"; then - print_info "No commits to check on ${current_branch} (compared to ${default_branch})" - return 0 - fi - - if ! command -v conform >/dev/null 2>&1; then - print_warning "conform not found in PATH - skipping commit linting" - echo " Install: mise install" - return 0 - fi - - if conform enforce --base-branch="${default_branch}" 2>/dev/null; then - print_success "Commit health check passed" - return 0 - else - print_error "Commit health check failed - check your commit messages" - return 1 - fi + print_header "COMMIT HEALTH (GOMMITLINT)" + + local current_branch default_branch + current_branch=$(git branch --show-current) + default_branch=$(get_default_branch) + + # 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" + return 0 + fi + + if ! has_commits_since "$default_branch"; then + print_info "No commits to check on ${current_branch} (compared to ${default_branch})" + return 0 + fi + + # 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" + return 0 + fi + + if $gommitlint_cmd validate --base-branch="${default_branch}" 2>/dev/null; then + print_success "Commit health check passed" + return 0 + else + print_error "Commit health check failed - check your commit messages" + return 1 + fi } main 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..ebe4b4d 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -17,149 +17,149 @@ source "${SCRIPT_DIR}/../summary/common.sh" # Base linters LINTERS=( - "Commits|conform|just lint-commits" - "Secrets|gitleaks|just lint-secrets" - "YAML|yamlfmt|just lint-yaml" - "Markdown|rumdl|just lint-markdown" - "Shell Scripts|shellcheck|just lint-shell" - "Shell Format|shfmt|just lint-shell-fmt" - "GitHub Actions|actionlint|just lint-actions" - "License|reuse|just lint-license" - "Containers|hadolint|just lint-container" - "XML|xmllint|just lint-xml" + "Commits|gommitlint|just lint-commits" + "Secrets|gitleaks|just lint-secrets" + "YAML|yamlfmt|just lint-yaml" + "Markdown|rumdl|just lint-markdown" + "Shell Scripts|shellcheck|just lint-shell" + "Shell Format|shfmt|just lint-shell-fmt" + "GitHub Actions|actionlint|just lint-actions" + "License|reuse|just lint-license" + "Containers|hadolint|just lint-container" + "XML|xmllint|just lint-xml" ) declare -A RESULTS declare -A OUTPUTS detect_language_linters() { - local recipes - recipes=$(just --list 2>&1 || true) - - # Java linters - check for individual recipes - if grep -qE "^\s+lint-java-checkstyle(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Java Checkstyle|checkstyle|just lint-java-checkstyle") - fi - if grep -qE "^\s+lint-java-pmd(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Java PMD|pmd|just lint-java-pmd") - fi - if grep -qE "^\s+lint-java-spotbugs(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Java SpotBugs|spotbugs|just lint-java-spotbugs") - fi - - # Node linters - check for individual recipes - if grep -qE "^\s+lint-node-eslint(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Node ESLint|eslint|just lint-node-eslint") - fi - if grep -qE "^\s+lint-node-format(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Node Format|prettier|just lint-node-format") - fi - if grep -qE "^\s+lint-node-ts-types(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Node Types|tsc|just lint-node-ts-types") - fi + local recipes + recipes=$(just --list 2>&1 || true) + + # Java linters - check for individual recipes + if grep -qE "^\s+lint-java-checkstyle(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Java Checkstyle|checkstyle|just lint-java-checkstyle") + fi + if grep -qE "^\s+lint-java-pmd(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Java PMD|pmd|just lint-java-pmd") + fi + if grep -qE "^\s+lint-java-spotbugs(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Java SpotBugs|spotbugs|just lint-java-spotbugs") + fi + + # Node linters - check for individual recipes + if grep -qE "^\s+lint-node-eslint(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Node ESLint|eslint|just lint-node-eslint") + fi + if grep -qE "^\s+lint-node-format(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Node Format|prettier|just lint-node-format") + fi + if grep -qE "^\s+lint-node-ts-types(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Node Types|tsc|just lint-node-ts-types") + fi } run_linters() { - local failed=0 - - 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" - - # Store output for summary module - OUTPUTS["$check"]="$output" - - # Parse status from output - local status details - if [[ $exit_code -eq 0 ]]; then - if [[ -z "${output// /}" ]]; then - # Empty output = linter disabled, skip entirely - status="disabled" - details="" - elif grep -q "not found in PATH" <<<"$output"; then - status="skip" - details="not in PATH" - elif grep -qiE "Skipping|Skip" <<<"$output"; then - status="skip" - details="skipped" - elif grep -qE "No .* (files? found|to check)|no commits to check" <<<"$output"; then - status="n/a" - details="n/a" - else - status="pass" - details=$(grep -oE "[0-9]+ (files?|commits|workflows)" <<<"$output" | head -1) - : "${details:=ok}" - fi - else - status="fail" - details="failed" - ((failed++)) - fi - - RESULTS["$check"]="$status|$tool|$details" - done - - return $failed + local failed=0 + + 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" + + # Store output for summary module + OUTPUTS["$check"]="$output" + + # Parse status from output + local status details + if [[ $exit_code -eq 0 ]]; then + if [[ -z "${output// /}" ]]; then + # Empty output = linter disabled, skip entirely + status="disabled" + details="" + elif grep -q "not found in PATH" <<<"$output"; then + status="skip" + details="not in PATH" + elif grep -qiE "Skipping|Skip" <<<"$output"; then + status="skip" + details="skipped" + elif grep -qE "No .* (files? found|to check)|no commits to check" <<<"$output"; then + status="n/a" + details="n/a" + else + status="pass" + details=$(grep -oE "[0-9]+ (files?|commits|workflows)" <<<"$output" | head -1) + : "${details:=ok}" + fi + else + status="fail" + details="failed" + ((failed++)) + fi + + RESULTS["$check"]="$status|$tool|$details" + done + + return $failed } print_summary() { - local passed=0 skipped=0 na=0 failed=0 + local passed=0 skipped=0 na=0 failed=0 - # Count results first - for linter_def in "${LINTERS[@]}"; do - IFS='|' read -r check_name _ _ <<<"$linter_def" - IFS='|' read -r status _ _ <<<"${RESULTS[$check_name]}" + # Count results first + for linter_def in "${LINTERS[@]}"; do + IFS='|' read -r check_name _ _ <<<"$linter_def" + IFS='|' read -r status _ _ <<<"${RESULTS[$check_name]}" - case "$status" in - pass) ((passed++)) ;; - skip) ((skipped++)) ;; - n/a) ((na++)) ;; - fail) ((failed++)) ;; - esac - done + case "$status" in + pass) ((passed++)) ;; + skip) ((skipped++)) ;; + n/a) ((na++)) ;; + fail) ((failed++)) ;; + esac + done - # Initialize summary module - local total=$((passed + failed + skipped + na)) - summary_init "$total" + # Initialize summary module + local total=$((passed + failed + skipped + na)) + summary_init "$total" - # Add results to summary module - for linter_def in "${LINTERS[@]}"; do - IFS='|' read -r check_name tool _ <<<"$linter_def" - IFS='|' read -r status real_tool raw_details <<<"${RESULTS[$check_name]}" + # Add results to summary module + for linter_def in "${LINTERS[@]}"; do + IFS='|' read -r check_name tool _ <<<"$linter_def" + IFS='|' read -r status real_tool raw_details <<<"${RESULTS[$check_name]}" - # Skip disabled linters entirely - [[ "$status" == "disabled" ]] && continue + # Skip disabled linters entirely + [[ "$status" == "disabled" ]] && continue - # Get output for this linter (for error details in GitHub summary) - local output="${OUTPUTS[$check_name]:-}" + # Get output for this linter (for error details in GitHub summary) + local output="${OUTPUTS[$check_name]:-}" - summary_add_result "$check_name" "$real_tool" "$status" "0" "$raw_details" "$output" - done + summary_add_result "$check_name" "$real_tool" "$status" "0" "$raw_details" "$output" + done - # Finalize summary - summary_finalize "$total" "$passed" "$failed" "$skipped" - local summary_exit=$? + # Finalize summary + summary_finalize "$total" "$passed" "$failed" "$skipped" + local summary_exit=$? - return $summary_exit + return $summary_exit } main() { - # Load appropriate summary module based on CI environment - load_summary_module + # Load appropriate summary module based on CI environment + load_summary_module - detect_language_linters + detect_language_linters - run_linters - local linter_exit=$? + run_linters + local linter_exit=$? - print_summary - local summary_exit=$? + print_summary + local summary_exit=$? - # Exit with failure if either failed - return $((linter_exit || summary_exit)) + # Exit with failure if either failed + return $((linter_exit || summary_exit)) } main diff --git a/tests/linters-commits.bats b/tests/linters-commits.bats index 98d16c2..6acb107 100644 --- a/tests/linters-commits.bats +++ b/tests/linters-commits.bats @@ -20,7 +20,7 @@ setup() { } teardown() { - unstub conform 2>/dev/null || true + unstub gommitlint 2>/dev/null || true common_teardown } From 67cbe2e4e57ed7bc25ddc7ed9396fcc0f7615fa5 Mon Sep 17 00:00:00 2001 From: Josef Andersson Date: Wed, 4 Mar 2026 20:18:07 +0100 Subject: [PATCH 4/7] chore: ignore IntelliJ settings Signed-off-by: Eric Thelin Signed-off-by: Josef Andersson --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index e6e7954..32d599b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ tests/libs/ + +#IntelliJ +/*.iml +/.idea/ From 16282ae90bc00e27acd2a23ef0126c11cf8edf14 Mon Sep 17 00:00:00 2001 From: Josef Andersson Date: Wed, 4 Mar 2026 20:18:07 +0100 Subject: [PATCH 5/7] feat: add linter for version control When running our automated checks we want to ensure that everything is version controlled. Otherwise there is a risk that we get false results, i.e. false positives or false negatives. This is essentially a port of the corresponding check we had in the code_quality.sh script. Signed-off-by: Eric Thelin Signed-off-by: Josef Andersson --- README.md | 2 + justfile | 5 +++ linters/version-control.sh | 36 ++++++++++++++++++ scripts/verify.sh | 1 + tests/linters-version-control.bats | 60 ++++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100755 linters/version-control.sh create mode 100644 tests/linters-version-control.bats diff --git a/README.md b/README.md index 12d9f67..3a8b128 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Run on every project. Skip automatically if no relevant files found: | Recipe | Tool | Checks | Skips when | |--------|------|--------|------------| +| `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 | @@ -438,6 +439,7 @@ devbase-check/ │ ├── secrets.sh │ ├── shell-fmt.sh │ ├── shell.sh +│ ├── version-control.sh │ ├── xml.sh │ └── yaml.sh ├── scripts/ diff --git a/justfile b/justfile index 803ccc0..4502a26 100644 --- a/justfile +++ b/justfile @@ -58,6 +58,11 @@ lint-base: [group('lint')] lint-all: lint-base +# Validate version control +[group('lint')] +lint-version-control: + @{{lint}}/version-control.sh + # Validate commit messages (gommitlint) [group('lint')] lint-commits: diff --git a/linters/version-control.sh b/linters/version-control.sh new file mode 100755 index 0000000..37d5bcf --- /dev/null +++ b/linters/version-control.sh @@ -0,0 +1,36 @@ +#!/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" + +main() { + print_header "VERSION CONTROL" + + if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + print_error "Not a Git repository - cannot verify version control state" + return 1 + fi + + if [[ -z "$(git status --porcelain)" ]]; then + print_success "All changes are under version control" + 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." + return 1 + fi +} + +main diff --git a/scripts/verify.sh b/scripts/verify.sh index ebe4b4d..997e6d7 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -17,6 +17,7 @@ source "${SCRIPT_DIR}/../summary/common.sh" # Base linters LINTERS=( + "Version Control|git|just lint-version-control" "Commits|gommitlint|just lint-commits" "Secrets|gitleaks|just lint-secrets" "YAML|yamlfmt|just lint-yaml" diff --git a/tests/linters-version-control.bats b/tests/linters-version-control.bats new file mode 100644 index 0000000..0fee5f5 --- /dev/null +++ b/tests/linters-version-control.bats @@ -0,0 +1,60 @@ +#!/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" + 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" +} + +@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 "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 "Not a Git repository - cannot verify version control state" + + rm -rf "$nonrepo_dir" +} From f939d93afb85111073e0099ad99d1b7ee3a31dd7 Mon Sep 17 00:00:00 2001 From: Josef Andersson Date: Wed, 4 Mar 2026 20:18:07 +0100 Subject: [PATCH 6/7] fix: catch linter output in general way Signed-off-by: Josef Andersson --- linters/commits.sh | 85 +++++---- linters/container.sh | 10 ++ linters/github-actions.sh | 10 ++ linters/java/checkstyle.sh | 10 ++ linters/java/format.sh | 13 ++ linters/java/lint.sh | 11 ++ linters/java/pmd.sh | 10 ++ linters/java/spotbugs.sh | 10 ++ linters/java/test.sh | 10 ++ linters/license.sh | 9 + linters/markdown.sh | 13 ++ linters/node/eslint.sh | 11 ++ linters/node/format.sh | 14 ++ linters/node/lint.sh | 8 + linters/node/types.sh | 11 ++ linters/secrets.sh | 9 + linters/shell-fmt.sh | 13 ++ linters/shell.sh | 10 ++ linters/version-control.sh | 33 ++-- linters/xml.sh | 10 ++ linters/yaml.sh | 13 ++ scripts/verify.sh | 273 +++++++++++++++++------------ tests/linters-commits.bats | 2 + tests/linters-java.bats | 32 ++++ tests/linters-markdown.bats | 16 ++ tests/linters-node.bats | 21 +++ tests/linters-secrets.bats | 3 + tests/linters-version-control.bats | 4 + tests/linters-yaml.bats | 3 + tests/verify.bats | 72 ++++++++ 30 files changed, 584 insertions(+), 165 deletions(-) diff --git a/linters/commits.sh b/linters/commits.sh index 6e44d47..259f656 100755 --- a/linters/commits.sh +++ b/linters/commits.sh @@ -10,44 +10,55 @@ 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 (GOMMITLINT)" - - local current_branch default_branch - current_branch=$(git branch --show-current) - default_branch=$(get_default_branch) - - # 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" - return 0 - fi - - if ! has_commits_since "$default_branch"; then - print_info "No commits to check on ${current_branch} (compared to ${default_branch})" - return 0 - fi - - # 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" - return 0 - fi - - if $gommitlint_cmd validate --base-branch="${default_branch}" 2>/dev/null; then - print_success "Commit health check passed" - return 0 - else - print_error "Commit health check failed - check your commit messages" - return 1 - fi + print_header "COMMIT HEALTH (GOMMITLINT)" + + local current_branch default_branch + current_branch=$(git branch --show-current) + default_branch=$(get_default_branch) + + # 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})" + emit_status "na" "n/a" + return 0 + fi + + # 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 $gommitlint_cmd validate --base-branch="${default_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 } main 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 index 37d5bcf..f903517 100755 --- a/linters/version-control.sh +++ b/linters/version-control.sh @@ -9,19 +9,27 @@ 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 "VERSION CONTROL" + 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" - return 1 - fi + 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" - return 0 - else - print_error "Some changes are not under version control! + 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 @@ -29,8 +37,9 @@ main() { 2. A linter automatically fixed a problem or reformatted the code. Please accept or discard any outstanding changes and try again." - return 1 - fi + 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 02f28c0..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" @@ -57,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 } @@ -70,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 } @@ -85,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 @@ -100,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/scripts/verify.sh b/scripts/verify.sh index 997e6d7..40d0e57 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -17,150 +17,191 @@ source "${SCRIPT_DIR}/../summary/common.sh" # Base linters LINTERS=( - "Version Control|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" - "Shell Scripts|shellcheck|just lint-shell" - "Shell Format|shfmt|just lint-shell-fmt" - "GitHub Actions|actionlint|just lint-actions" - "License|reuse|just lint-license" - "Containers|hadolint|just lint-container" - "XML|xmllint|just lint-xml" + "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" + "Shell Scripts|shellcheck|just lint-shell" + "Shell Format|shfmt|just lint-shell-fmt" + "GitHub Actions|actionlint|just lint-actions" + "License|reuse|just lint-license" + "Containers|hadolint|just lint-container" + "XML|xmllint|just lint-xml" ) 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) - - # Java linters - check for individual recipes - if grep -qE "^\s+lint-java-checkstyle(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Java Checkstyle|checkstyle|just lint-java-checkstyle") - fi - if grep -qE "^\s+lint-java-pmd(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Java PMD|pmd|just lint-java-pmd") - fi - if grep -qE "^\s+lint-java-spotbugs(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Java SpotBugs|spotbugs|just lint-java-spotbugs") - fi - - # Node linters - check for individual recipes - if grep -qE "^\s+lint-node-eslint(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Node ESLint|eslint|just lint-node-eslint") - fi - if grep -qE "^\s+lint-node-format(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Node Format|prettier|just lint-node-format") - fi - if grep -qE "^\s+lint-node-ts-types(\s|#|$)" <<<"$recipes"; then - LINTERS+=("Node Types|tsc|just lint-node-ts-types") - fi + local recipes + recipes=$(just --list 2>&1 || true) + + # Java linters - check for individual recipes + if grep -qE "^\s+lint-java-checkstyle(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Java Checkstyle|checkstyle|just lint-java-checkstyle") + fi + if grep -qE "^\s+lint-java-pmd(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Java PMD|pmd|just lint-java-pmd") + fi + if grep -qE "^\s+lint-java-spotbugs(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Java SpotBugs|spotbugs|just lint-java-spotbugs") + fi + + # Node linters - check for individual recipes + if grep -qE "^\s+lint-node-eslint(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Node ESLint|eslint|just lint-node-eslint") + fi + if grep -qE "^\s+lint-node-format(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Node Format|prettier|just lint-node-format") + fi + if grep -qE "^\s+lint-node-ts-types(\s|#|$)" <<<"$recipes"; then + LINTERS+=("Node Types|tsc|just lint-node-ts-types") + fi } run_linters() { - local failed=0 - - 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" - - # Store output for summary module - OUTPUTS["$check"]="$output" - - # Parse status from output - local status details - if [[ $exit_code -eq 0 ]]; then - if [[ -z "${output// /}" ]]; then - # Empty output = linter disabled, skip entirely - status="disabled" - details="" - elif grep -q "not found in PATH" <<<"$output"; then - status="skip" - details="not in PATH" - elif grep -qiE "Skipping|Skip" <<<"$output"; then - status="skip" - details="skipped" - elif grep -qE "No .* (files? found|to check)|no commits to check" <<<"$output"; then - status="n/a" - details="n/a" - else - status="pass" - details=$(grep -oE "[0-9]+ (files?|commits|workflows)" <<<"$output" | head -1) - : "${details:=ok}" - fi - else - status="fail" - details="failed" - ((failed++)) - fi - - RESULTS["$check"]="$status|$tool|$details" - done - - return $failed + local failed=0 + + for linter_def in "${LINTERS[@]}"; do + IFS='|' read -r check tool cmd <<<"$linter_def" + + 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 + 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" + details="" + elif grep -q "not found in PATH" <<<"$output"; then + status="skip" + details="not in PATH" + 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 + status="n/a" + details="n/a" + else + status="pass" + details=$(grep -oE "[0-9]+ (files?|commits|workflows)" <<<"$output" | head -1) + : "${details:=ok}" + fi + else + status="fail" + details="failed" + ((failed++)) + fi + + RESULTS["$check"]="$status|$tool|$details" + done + + return $failed } print_summary() { - local passed=0 skipped=0 na=0 failed=0 + local passed=0 skipped=0 na=0 failed=0 - # Count results first - for linter_def in "${LINTERS[@]}"; do - IFS='|' read -r check_name _ _ <<<"$linter_def" - IFS='|' read -r status _ _ <<<"${RESULTS[$check_name]}" + # Count results first + for linter_def in "${LINTERS[@]}"; do + IFS='|' read -r check_name _ _ <<<"$linter_def" + IFS='|' read -r status _ _ <<<"${RESULTS[$check_name]}" - case "$status" in - pass) ((passed++)) ;; - skip) ((skipped++)) ;; - n/a) ((na++)) ;; - fail) ((failed++)) ;; - esac - done + case "$status" in + pass) ((passed++)) ;; + skip) ((skipped++)) ;; + n/a) ((na++)) ;; + fail) ((failed++)) ;; + esac + done - # Initialize summary module - local total=$((passed + failed + skipped + na)) - summary_init "$total" + # Initialize summary module + local total=$((passed + failed + skipped + na)) + summary_init "$total" - # Add results to summary module - for linter_def in "${LINTERS[@]}"; do - IFS='|' read -r check_name tool _ <<<"$linter_def" - IFS='|' read -r status real_tool raw_details <<<"${RESULTS[$check_name]}" + # Add results to summary module + for linter_def in "${LINTERS[@]}"; do + IFS='|' read -r check_name tool _ <<<"$linter_def" + IFS='|' read -r status real_tool raw_details <<<"${RESULTS[$check_name]}" - # Skip disabled linters entirely - [[ "$status" == "disabled" ]] && continue + # Skip disabled linters entirely + [[ "$status" == "disabled" ]] && continue - # Get output for this linter (for error details in GitHub summary) - local output="${OUTPUTS[$check_name]:-}" + # Get output for this linter (for error details in GitHub summary) + local output="${OUTPUTS[$check_name]:-}" - summary_add_result "$check_name" "$real_tool" "$status" "0" "$raw_details" "$output" - done + summary_add_result "$check_name" "$real_tool" "$status" "0" "$raw_details" "$output" + done - # Finalize summary - summary_finalize "$total" "$passed" "$failed" "$skipped" - local summary_exit=$? + # Finalize summary + summary_finalize "$total" "$passed" "$failed" "$skipped" + local summary_exit=$? - return $summary_exit + return $summary_exit } main() { - # Load appropriate summary module based on CI environment - load_summary_module + # Load appropriate summary module based on CI environment + load_summary_module - detect_language_linters + detect_language_linters - run_linters - local linter_exit=$? + run_linters + local linter_exit=$? - print_summary - local summary_exit=$? + print_summary + local summary_exit=$? - # Exit with failure if either failed - return $((linter_exit || summary_exit)) + # Exit with failure if either failed + return $((linter_exit || summary_exit)) } main diff --git a/tests/linters-commits.bats b/tests/linters-commits.bats index 6acb107..d93ed60 100644 --- a/tests/linters-commits.bats +++ b/tests/linters-commits.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 } @@ -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 index 0fee5f5..64ac9ca 100644 --- a/tests/linters-version-control.bats +++ b/tests/linters-version-control.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 } @@ -29,6 +30,7 @@ teardown() { [ "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" { @@ -37,6 +39,7 @@ teardown() { [ "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 @@ -54,6 +57,7 @@ teardown() { [ "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 fd2212d..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"* ]] } 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" +} From c67ce29f8f37a3b959205044056bce47582cfb1d Mon Sep 17 00:00:00 2001 From: Josef Andersson Date: Wed, 4 Mar 2026 20:22:58 +0100 Subject: [PATCH 7/7] fix: improve commit handling Signed-off-by: Josef Andersson --- .gommitlint.yaml | 4 +++- linters/commits.sh | 21 +++++++++++++++++---- utils/git-utils.sh | 11 +++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.gommitlint.yaml b/.gommitlint.yaml index 69600ad..ec68bb0 100644 --- a/.gommitlint.yaml +++ b/.gommitlint.yaml @@ -5,4 +5,6 @@ gommitlint: crypto_signature: required: true - require_gpg: true + require_ssh: true + repo: + max_commits_ahead: 10 diff --git a/linters/commits.sh b/linters/commits.sh index 259f656..8ebc261 100755 --- a/linters/commits.sh +++ b/linters/commits.sh @@ -19,9 +19,22 @@ emit_status() { main() { 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 (gommitlint can't handle base..HEAD when they're the same) if [[ "$current_branch" == "$default_branch" ]]; then @@ -30,8 +43,8 @@ main() { 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 @@ -50,7 +63,7 @@ main() { return 0 fi - if $gommitlint_cmd validate --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 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 }