Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ac8b783
feat: implement issue #61 — Compliance: codeowners-org-leads-not-first
donpetry-bot Jun 10, 2026
eab6d7e
chore: apply manual instructions [skip ci-relay]
donpetry-bot Jun 10, 2026
cab4154
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 10, 2026
780fe17
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 10, 2026
0fcb2ec
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 10, 2026
9d64239
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 10, 2026
dd11300
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 11, 2026
d354c5d
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 11, 2026
f88deaf
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 11, 2026
dd3b18c
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 11, 2026
e33fdc6
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 11, 2026
90787a4
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 11, 2026
7de75cd
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 11, 2026
e47a6f1
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 12, 2026
9eaf435
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 12, 2026
979c1f5
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 12, 2026
eb7fa4c
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 12, 2026
cbabe7e
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 12, 2026
c50ff37
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 13, 2026
e94a595
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 13, 2026
9b23306
Merge branch 'main' into dev-lead/issue-61-20260610-1417
github-actions[bot] Jun 13, 2026
e0ab48e
Merge branch 'main' into dev-lead/issue-61-20260610-1417
github-actions[bot] Jun 13, 2026
a69422d
Merge branch 'main' into dev-lead/issue-61-20260610-1417
github-actions[bot] Jun 13, 2026
597c642
Merge branch 'main' into dev-lead/issue-61-20260610-1417
github-actions[bot] Jun 13, 2026
340cfdf
Merge branch 'main' into dev-lead/issue-61-20260610-1417
github-actions[bot] Jun 13, 2026
6616494
Merge branch 'main' into dev-lead/issue-61-20260610-1417
don-petry Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions scripts/dev-lead-lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment on lines +108 to +119

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Running printf and awk in a subshell for every line of the file is highly inefficient in Bash because it forks two new processes per line. This can make the lint script noticeably slow, especially on Windows or macOS environments.

Additionally, this approach has two correctness issues:

  1. CRLF Line Endings: If the CODEOWNERS file has CRLF (\r\n) line endings, the trailing \r will remain on the line and cause the validation to fail.
  2. Escaped Spaces: If a pattern contains escaped spaces (e.g., path\\ with\\ spaces/ @owner), awk will split on the space and incorrectly identify the second part of the path as the owner.

We can resolve all of these issues efficiently using pure Bash built-ins and array splitting.

Suggested change
while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*$ ]] && continue
[[ "$line" =~ ^[[:space:]]*# ]] && continue
first_owner=$(printf '%s\n' "$line" | awk '{print $2}')
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"
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"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the suggestion in scripts/dev-lead-lint.sh (lines 108–116): replaced the printf '%s\n' "$line" | awk '{print $2}' subshell with pure Bash builtins — added line="${line%$'\r'}" to strip CRLF, clean_line="${line//\\ /}" to handle escaped spaces in paths, and read -r -a parts <<< "$clean_line" to split on whitespace without forking processes. Also added three new bats tests in tests/dev-lead/unit/test_dev_lead_lint.bats covering CRLF handling, escaped-space paths, and CRLF-with-wrong-owner; all 22 tests pass.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed implemented. scripts/dev-lead-lint.sh lines 108–119 now use pure Bash builtins matching the suggestion exactly: line="${line%$'\r'}" strips CRLF, clean_line="${line//\\ /}" collapses escaped spaces before splitting, and read -r -a parts <<< "$clean_line" replaces the per-line printf | awk subshell. Three corresponding bats tests (lines 256–279 in tests/dev-lead/unit/test_dev_lead_lint.bats) cover CRLF-clean, escaped-space paths, and CRLF-with-wrong-owner. ShellCheck passes with no warnings.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified as implemented: scripts/dev-lead-lint.sh lines 108–119 now match the suggestion exactly — line="${line%$'\r'}" strips CRLF, clean_line="${line//\\ /}" collapses escaped spaces, and read -r -a parts <<< "$clean_line" replaces the per-line printf | awk subshell. Three bats tests (lines 256–279 in tests/dev-lead/unit/test_dev_lead_lint.bats) cover CRLF-clean, escaped-space paths, and CRLF-with-wrong-owner. ShellCheck passes with no warnings.

[ "$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
Expand Down
83 changes: 83 additions & 0 deletions tests/dev-lead/unit/test_dev_lead_lint.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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" ]]
}
Loading