diff --git a/scripts/dev-lead-lint.sh b/scripts/dev-lead-lint.sh index c85b52869..3845d0c4f 100755 --- a/scripts/dev-lead-lint.sh +++ b/scripts/dev-lead-lint.sh @@ -91,6 +91,36 @@ if [ "${#agent_files[@]}" -gt 0 ]; then fi fi +# ── 3. CODEOWNERS validation ────────────────────────────────────────────────── +# Checks that every owner line lists @petry-projects/org-leads first. +# Standard: standards/codeowners-standard.md (codeowners-org-leads-not-first check) +codeowners_file="" +for candidate in ".github/CODEOWNERS" "CODEOWNERS" "docs/CODEOWNERS"; do + if [ -f "$candidate" ]; then + codeowners_file="$candidate" + break + fi +done + +if [ -n "$codeowners_file" ]; then + echo " [lint] validating $codeowners_file..." + codeowners_fail=0 + while IFS= read -r line; do + line="${line%$'\r'}" + [[ "$line" =~ ^[[:space:]]*$ ]] && continue + [[ "$line" =~ ^[[:space:]]*# ]] && continue + clean_line="${line//\\ /}" + read -r -a parts <<< "$clean_line" + first_owner="${parts[1]:-}" + if [ "$first_owner" != "@petry-projects/org-leads" ]; then + echo "FAIL: $codeowners_file — @petry-projects/org-leads must be the first owner on line: $line" + codeowners_fail=1 + fi + done < "$codeowners_file" + [ "$codeowners_fail" -eq 0 ] || fail=1 + [ "$codeowners_fail" -eq 0 ] && echo "OK: $codeowners_file" +fi + if [ "$fail" -eq 0 ]; then echo " [lint] all checks passed" fi diff --git a/tests/dev-lead/unit/test_dev_lead_lint.bats b/tests/dev-lead/unit/test_dev_lead_lint.bats index 958f9f1b7..50c776b09 100644 --- a/tests/dev-lead/unit/test_dev_lead_lint.bats +++ b/tests/dev-lead/unit/test_dev_lead_lint.bats @@ -194,3 +194,86 @@ MD run bash "$LINT_SCRIPT" [ "$status" -eq 0 ] } + +# ── CODEOWNERS validation tests ─────────────────────────────────────────────── + +@test "lint: passes when no CODEOWNERS file exists" { + run bash "$LINT_SCRIPT" + [ "$status" -eq 0 ] +} + +@test "lint: passes when .github/CODEOWNERS lists org-leads first" { + mkdir -p "$WORK_DIR/.github" + cat > "$WORK_DIR/.github/CODEOWNERS" <<'CO' +# Default owner +* @petry-projects/org-leads +CO + run bash "$LINT_SCRIPT" + [ "$status" -eq 0 ] +} + +@test "lint: passes when CODEOWNERS lists org-leads first with additional owners" { + mkdir -p "$WORK_DIR/.github" + cat > "$WORK_DIR/.github/CODEOWNERS" <<'CO' +* @petry-projects/org-leads @petry-projects/backend +docs/ @petry-projects/org-leads @petry-projects/docs +CO + run bash "$LINT_SCRIPT" + [ "$status" -eq 0 ] +} + +@test "lint: fails when CODEOWNERS owner line does not list org-leads first" { + mkdir -p "$WORK_DIR/.github" + cat > "$WORK_DIR/.github/CODEOWNERS" <<'CO' +* @petry-projects/backend @petry-projects/org-leads +CO + run bash "$LINT_SCRIPT" + [ "$status" -ne 0 ] + [[ "$output" =~ "org-leads" ]] +} + +@test "lint: fails when CODEOWNERS owner line omits org-leads entirely" { + mkdir -p "$WORK_DIR/.github" + cat > "$WORK_DIR/.github/CODEOWNERS" <<'CO' +* @petry-projects/backend +CO + run bash "$LINT_SCRIPT" + [ "$status" -ne 0 ] + [[ "$output" =~ "org-leads" ]] +} + +@test "lint: ignores comment lines and blank lines in CODEOWNERS" { + mkdir -p "$WORK_DIR/.github" + cat > "$WORK_DIR/.github/CODEOWNERS" <<'CO' +# This is a comment + +* @petry-projects/org-leads +CO + run bash "$LINT_SCRIPT" + [ "$status" -eq 0 ] +} + +@test "lint: handles CRLF line endings in CODEOWNERS" { + mkdir -p "$WORK_DIR/.github" + printf '* @petry-projects/org-leads\r\ndocs/ @petry-projects/org-leads @petry-projects/docs\r\n' \ + > "$WORK_DIR/.github/CODEOWNERS" + run bash "$LINT_SCRIPT" + [ "$status" -eq 0 ] +} + +@test "lint: handles escaped spaces in CODEOWNERS paths" { + mkdir -p "$WORK_DIR/.github" + printf 'path\\ with\\ spaces/ @petry-projects/org-leads @petry-projects/backend\n' \ + > "$WORK_DIR/.github/CODEOWNERS" + run bash "$LINT_SCRIPT" + [ "$status" -eq 0 ] +} + +@test "lint: fails with CRLF when org-leads is not first" { + mkdir -p "$WORK_DIR/.github" + printf '* @petry-projects/backend @petry-projects/org-leads\r\n' \ + > "$WORK_DIR/.github/CODEOWNERS" + run bash "$LINT_SCRIPT" + [ "$status" -ne 0 ] + [[ "$output" =~ "org-leads" ]] +}