Skip to content

Commit 9d107ad

Browse files
committed
feat: Scope local-clone scanners to the core-repos allowlist
Wire the link-health and dep-bump discovery loops to the shared allowlist, so they act only on the curated core repos instead of every locally cloned directory. - program-lib.sh: add is_core_repo() (exact whole-line membership test). Document canonical_repo_for_dir() as a TRANSITIONAL shim (see #37). - Each clone-iteration loop now: maps the dir basename to its canonical repo, skips non-core/archived repos (allowlist), and dedups duplicate clone dirs (e.g. stale kagenti/ alongside rossoctl/) so each canonical repo is processed once. API refs are built as rossoctl/<canonical>. - dep-bump-scanner: this fixes the false "0 open Dependabot PRs" report -- gh pr list --author silently returns empty across a rename redirect, so querying the canonical repo restores results (rossoctl/rossoctl now returns 11 vs 0). - extract-broken-links.sh: arg 2 is now a full owner/name ref emitted verbatim (was a bare name prefixed with kagenti/); test updated. Scope: discovery/read paths only. Fixer WRITE paths (ensure_fork / create_fork_pr / $ORG in PR creation) are intentionally left for Phase 6, where they are resolved alongside the fork-naming decision. Verified against the real host clone set: 8 core repos scanned (from 25), duplicate clones deduped, archived/non-core excluded; full test suite passes on bash 3.2. Part of #29 (epic #32). Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
1 parent d16970d commit 9d107ad

8 files changed

Lines changed: 137 additions & 23 deletions

scripts/dep-bump-fixer.sh

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,25 @@ if [ ! -f "$REPORTS_DIR/baseline.json" ]; then
9393

9494
# Query merged Dependabot PRs across all repos (last 90 days)
9595
: > "$TMPDIR/merged_prs.jsonl"
96+
seen_canon=""
9697
for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do
9798
[ -d "$repo_dir" ] || continue
9899
repo_name=$(basename "$repo_dir")
99100
if [[ "$repo_name" == .* && "$repo_name" != ".github" ]] || [ ! -d "$repo_dir/.git" ]; then
100101
continue
101102
fi
102103

103-
gh pr list --repo "$ORG/$repo_name" \
104+
# Core-repo allowlist + canonical remap + dedup of duplicate clone dirs.
105+
canon=$(canonical_repo_for_dir "$repo_name")
106+
if ! is_core_repo "$canon"; then
107+
continue
108+
fi
109+
case " $seen_canon " in
110+
*" $canon "*) continue ;;
111+
esac
112+
seen_canon="$seen_canon $canon"
113+
114+
gh pr list --repo "rossoctl/$canon" \
104115
--author "app/dependabot" \
105116
--state merged \
106117
--json number,createdAt,mergedAt \
@@ -149,15 +160,26 @@ echo "--- Discovering scanner issues ---"
149160
: > "$TMPDIR/issues.jsonl"
150161
REPOS_CHECKED=0
151162

163+
SEEN_CANON=""
152164
for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do
153165
[ -d "$repo_dir" ] || continue
154166
repo_name=$(basename "$repo_dir")
155167
if [[ "$repo_name" == .* && "$repo_name" != ".github" ]] || [ ! -d "$repo_dir/.git" ]; then
156168
continue
157169
fi
158170

171+
# Core-repo allowlist + canonical remap + dedup of duplicate clone dirs.
172+
canon=$(canonical_repo_for_dir "$repo_name")
173+
if ! is_core_repo "$canon"; then
174+
continue
175+
fi
176+
case " $SEEN_CANON " in
177+
*" $canon "*) continue ;;
178+
esac
179+
SEEN_CANON="$SEEN_CANON $canon"
180+
159181
REPOS_CHECKED=$((REPOS_CHECKED + 1))
160-
full_repo="$ORG/$repo_name"
182+
full_repo="rossoctl/$canon"
161183

162184
issues_json=$(gh issue list --repo "$full_repo" \
163185
--search "[dep-bump] in:title" \
@@ -514,14 +536,25 @@ echo "--- Computing metrics ---"
514536

515537
# Query recently merged Dependabot PRs (last 30 days) for TTM
516538
: > "$TMPDIR/recent_merged.jsonl"
539+
SEEN_CANON_TTM=""
517540
for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do
518541
[ -d "$repo_dir" ] || continue
519542
repo_name=$(basename "$repo_dir")
520543
if [[ "$repo_name" == .* && "$repo_name" != ".github" ]] || [ ! -d "$repo_dir/.git" ]; then
521544
continue
522545
fi
523546

524-
gh pr list --repo "$ORG/$repo_name" \
547+
# Core-repo allowlist + canonical remap + dedup of duplicate clone dirs.
548+
canon=$(canonical_repo_for_dir "$repo_name")
549+
if ! is_core_repo "$canon"; then
550+
continue
551+
fi
552+
case " $SEEN_CANON_TTM " in
553+
*" $canon "*) continue ;;
554+
esac
555+
SEEN_CANON_TTM="$SEEN_CANON_TTM $canon"
556+
557+
gh pr list --repo "rossoctl/$canon" \
525558
--author "app/dependabot" \
526559
--state merged \
527560
--json number,createdAt,mergedAt \

scripts/dep-bump-scanner.sh

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ echo "--- Detecting ecosystems ---"
102102
: > "$TMPDIR/ecosystems.jsonl"
103103
REPOS_SCANNED=0
104104

105+
SEEN_CANON=""
105106
for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do
106107
[ -d "$repo_dir" ] || continue
107108
repo_name=$(basename "$repo_dir")
@@ -111,6 +112,17 @@ for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do
111112
continue
112113
fi
113114

115+
# Restrict to core repos (allowlist) via the canonical name; dedup duplicate
116+
# clone dirs so each canonical repo is processed once.
117+
canon=$(canonical_repo_for_dir "$repo_name")
118+
if ! is_core_repo "$canon"; then
119+
continue
120+
fi
121+
case " $SEEN_CANON " in
122+
*" $canon "*) continue ;;
123+
esac
124+
SEEN_CANON="$SEEN_CANON $canon"
125+
114126
REPOS_SCANNED=$((REPOS_SCANNED + 1))
115127
ecosystems=""
116128

@@ -147,7 +159,8 @@ for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do
147159
# Strip trailing comma
148160
ecosystems="${ecosystems%,}"
149161

150-
jq -nc --arg repo "$repo_name" --arg eco "$ecosystems" \
162+
# Store the canonical repo name so Step 2 builds correct rossoctl/<name> refs.
163+
jq -nc --arg repo "$canon" --arg eco "$ecosystems" \
151164
'{repo: $repo, ecosystems: ($eco | split(",") | map(select(. != "")))}' \
152165
>> "$TMPDIR/ecosystems.jsonl"
153166
done
@@ -163,12 +176,14 @@ REPOS_WITH_DEPENDABOT=0
163176
TOTAL_OPEN_PRS=0
164177

165178
while IFS= read -r eco_record; do
166-
repo_name=$(echo "$eco_record" | jq -r '.repo')
167-
full_repo="$ORG/$repo_name"
179+
repo_name=$(echo "$eco_record" | jq -r '.repo') # canonical name from Step 1
180+
full_repo="rossoctl/$repo_name"
168181

169182
echo " Checking $repo_name..."
170183

171-
# List open Dependabot PRs
184+
# List open Dependabot PRs. Query the canonical repo directly -- gh pr list
185+
# with --author silently returns empty across a rename redirect, which is why
186+
# the pre-rename names reported zero Dependabot PRs.
172187
prs_json=$(gh pr list --repo "$full_repo" \
173188
--author "app/dependabot" \
174189
--state open \
@@ -418,7 +433,8 @@ while IFS='|' read -r issue_repo issue_pr_number; do
418433
category=$(echo "$record" | jq -r '.category')
419434
overdue=$((age_days - sla_days))
420435

421-
full_repo="$ORG/$issue_repo"
436+
# Record repos are canonical names (see Step 1); build canonical refs.
437+
full_repo="rossoctl/$issue_repo"
422438

423439
# Deduplication
424440
search_term="[dep-bump] Stale $severity bump: $package in $issue_repo"
@@ -497,7 +513,7 @@ ISSUES_CLOSED=0
497513
while IFS='|' read -r fix_repo fix_pr_number; do
498514
[ -z "$fix_repo" ] && continue
499515

500-
full_repo="$ORG/$fix_repo"
516+
full_repo="rossoctl/$fix_repo"
501517

502518
# Find matching open issue by searching for the PR number in title/body
503519
issue_number=$(gh issue list --repo "$full_repo" \

scripts/extract-broken-links.sh

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@ set -euo pipefail
1414
# here so it can be unit-tested against synthetic lychee fixtures.
1515
#
1616
# Usage:
17-
# extract-broken-links.sh <lychee-json> <repo-name> <repos-prefix>
18-
# cat report.json | extract-broken-links.sh - <repo-name> <repos-prefix>
17+
# extract-broken-links.sh <lychee-json> <repo> <repos-prefix>
18+
# cat report.json | extract-broken-links.sh - <repo> <repos-prefix>
1919
#
2020
# Arguments:
2121
# <lychee-json> Path to a lychee JSON report, or - to read from stdin.
22-
# <repo-name> Bare repo name (e.g. "kagenti"); emitted as "kagenti/<name>".
22+
# <repo> Full "owner/name" repo reference (e.g. "rossoctl/cortex");
23+
# emitted verbatim in the "repo" field.
2324
# <repos-prefix> Absolute path prefix stripped from lychee's file keys
24-
# (e.g. "/home/claw/kagenti/kagenti/").
25+
# (e.g. "/home/claw/kagenti/rossoctl/").
2526
#
2627
# Output (stdout): JSONL, zero or more objects of the form
27-
# {"repo":"kagenti/foo","file":"docs/x.md","url":"https://...","status":"404","category":"external"}
28+
# {"repo":"rossoctl/foo","file":"docs/x.md","url":"https://...","status":"404","category":"external"}
2829
#
2930
# Exit codes:
3031
# 0 - success (may emit zero records)
@@ -38,7 +39,7 @@ if [ $# -lt 3 ]; then
3839
fi
3940

4041
LYCHEE_INPUT="$1"
41-
REPO_NAME="$2"
42+
REPO_FULL="$2" # full "owner/name" reference, emitted verbatim
4243
REPOS_PREFIX="$3"
4344

4445
if [ "$LYCHEE_INPUT" != "-" ] && [ ! -f "$LYCHEE_INPUT" ]; then
@@ -55,7 +56,7 @@ fi
5556
# Normalize lychee status to enum tokens: numeric codes stay as-is,
5657
# text statuses map to: timeout, dns, unreachable, error, unknown.
5758
# Suppress URLs with unreachable-by-design hostnames (cluster-local, .local, RFC1918).
58-
jq -r --arg repo "$REPO_NAME" --arg repos_dir "$REPOS_PREFIX" '
59+
jq -r --arg repo "$REPO_FULL" --arg repos_dir "$REPOS_PREFIX" '
5960
.error_map // {} | to_entries[] |
6061
.key as $filepath |
6162
.value[] |
@@ -79,7 +80,7 @@ jq -r --arg repo "$REPO_NAME" --arg repos_dir "$REPOS_PREFIX" '
7980
end
8081
) as $status |
8182
{
82-
repo: ("kagenti/" + $repo),
83+
repo: $repo,
8384
file: ($filepath | ltrimstr($repos_dir) | ltrimstr("./")),
8485
url: .url,
8586
status: $status,

scripts/link-health-fixer.sh

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,26 @@ echo "=== Step 1: Gathering open scanner issues ==="
6060
ISSUES_FILE="$TMPDIR/issues.jsonl"
6161
: > "$ISSUES_FILE"
6262

63+
SEEN_CANON=""
6364
for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do
6465
[ -d "$repo_dir" ] || continue
6566
repo_name=$(basename "$repo_dir")
6667
if [[ "$repo_name" == .* && "$repo_name" != ".github" ]] || [ ! -d "$repo_dir/.git" ]; then
6768
continue
6869
fi
6970

70-
full_repo="$ORG/$repo_name"
71+
# Restrict to core repos (allowlist), mapping pre-rename dir names first;
72+
# dedup so duplicate clone dirs for the same canonical repo run once.
73+
canon=$(canonical_repo_for_dir "$repo_name")
74+
if ! is_core_repo "$canon"; then
75+
continue
76+
fi
77+
case " $SEEN_CANON " in
78+
*" $canon "*) continue ;;
79+
esac
80+
SEEN_CANON="$SEEN_CANON $canon"
81+
82+
full_repo="rossoctl/$canon"
7183

7284
issues_json=$(gh issue list --repo "$full_repo" \
7385
--search "Broken link in:title" \

scripts/link-health-scanner.sh

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ REPOS_FAILED=0
6363
# Collect all broken links into a single JSONL file
6464
: > "$TMPDIR/broken.jsonl"
6565

66+
# Track canonical repos already scanned this run, so duplicate clone dirs
67+
# (e.g. a stale "kagenti" alongside "rossoctl") are not scanned twice.
68+
SEEN_CANON=""
69+
6670
for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do
6771
[ -d "$repo_dir" ] || continue
6872
repo_name=$(basename "$repo_dir")
@@ -72,7 +76,20 @@ for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do
7276
continue
7377
fi
7478

75-
echo "Scanning $repo_name..."
79+
# Restrict to core repos (allowlist), mapping pre-rename dir names to their
80+
# canonical repo first. Non-core / archived clones are skipped.
81+
canon=$(canonical_repo_for_dir "$repo_name")
82+
if ! is_core_repo "$canon"; then
83+
continue
84+
fi
85+
86+
# Dedup: skip if another clone dir already covered this canonical repo.
87+
case " $SEEN_CANON " in
88+
*" $canon "*) echo "Skipping $repo_name (already scanned as $canon)"; continue ;;
89+
esac
90+
SEEN_CANON="$SEEN_CANON $canon"
91+
92+
echo "Scanning $repo_name (as rossoctl/$canon)..."
7693

7794
LYCHEE_OUTPUT="$TMPDIR/lychee_${repo_name}.json"
7895

@@ -114,7 +131,7 @@ for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do
114131
# normalization logic lives in extract-broken-links.sh so it can be unit-tested
115132
# (see tests/test-extract-broken-links.sh).
116133
"$SCRIPT_DIR/extract-broken-links.sh" \
117-
"$LYCHEE_OUTPUT" "$repo_name" "$REPOS_DIR/$repo_name/" \
134+
"$LYCHEE_OUTPUT" "rossoctl/$canon" "$REPOS_DIR/$repo_name/" \
118135
>> "$TMPDIR/broken.jsonl" 2>/dev/null || true
119136

120137
echo " Links: $repo_total, Errors: $repo_errors"

scripts/program-lib.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,12 +841,35 @@ core_repo_names() {
841841
get_core_repos | sed 's|^[^/]*/||'
842842
}
843843

844+
# Return 0 if the given bare repo name is in the core allowlist, else 1.
845+
#
846+
# Intended for filtering local clone iteration: pass the canonical name
847+
# (see canonical_repo_for_dir) so pre-rename clone dirs are matched correctly.
848+
# Uses an exact, whole-line match (grep -Fx) to avoid substring false positives.
849+
#
850+
# Usage:
851+
# canon=$(canonical_repo_for_dir "$repo_name")
852+
# is_core_repo "$canon" || continue
853+
# Args:
854+
# $1 - bare repo name (no owner prefix)
855+
is_core_repo() {
856+
local name="$1"
857+
core_repo_names | grep -qxF "$name"
858+
}
859+
844860
# Map a local clone directory basename to its canonical bare repo name.
845861
#
846862
# Clone dirs may still use pre-rename names; this encapsulates the rename
847863
# remap table in one place so every script agrees. Unknown names pass through
848864
# unchanged (identity), so non-remapped repos need no special handling.
849865
#
866+
# TRANSITIONAL: the non-identity entries below are a temporary bridge for the
867+
# kagenti->rossoctl rename while stale-named clone dirs still exist on disk.
868+
# Once clone dirs are renamed to canonical names, this function becomes pure
869+
# identity and the entries should be deleted. See rossoctl/automation#37.
870+
# It is a lookup table, not a rename detector -- do not treat it as protection
871+
# against future renames.
872+
#
850873
# Returns: the bare repo name only (e.g. "rossoctl"), NOT an owner/name pair.
851874
# Prepend the owner to build a full API reference, e.g. "rossoctl/$canon".
852875
#

tests/test-core-repos.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ if CORE_REPOS_FILE="$TEST_TMPDIR/empty.txt" get_core_repos >/dev/null 2>&1; then
5252
echo "FAIL get_core_repos should error on empty allowlist"; fail=1
5353
fi
5454

55+
# --- is_core_repo: membership test against the allowlist (exact, whole-line) ---
56+
CORE_REPOS_FILE="$TEST_TMPDIR/good.txt" is_core_repo "rossoctl" \
57+
|| { echo "FAIL is_core_repo: rossoctl should be in allowlist"; fail=1; }
58+
if CORE_REPOS_FILE="$TEST_TMPDIR/good.txt" is_core_repo "not-a-repo"; then
59+
echo "FAIL is_core_repo: not-a-repo should NOT match"; fail=1
60+
fi
61+
# Guard against substring false positives (rosso is a prefix of rossoctl).
62+
if CORE_REPOS_FILE="$TEST_TMPDIR/good.txt" is_core_repo "rosso"; then
63+
echo "FAIL is_core_repo: partial 'rosso' must not match 'rossoctl'"; fail=1
64+
fi
65+
5566
# --- canonical_repo_for_dir: remaps the two renamed dirs, identity otherwise ---
5667
[ "$(canonical_repo_for_dir kagenti)" = "rossoctl" ] \
5768
|| { echo "FAIL canonical: kagenti -> rossoctl"; fail=1; }

tests/test-extract-broken-links.sh

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ EXTRACTOR="$SCRIPT_DIR/../scripts/extract-broken-links.sh"
1010
TEST_TMPDIR=$(mktemp -d "/tmp/test-extract-broken-XXXXXX")
1111
trap 'rm -rf "$TEST_TMPDIR"' EXIT
1212

13-
REPO="kagenti-extensions"
14-
REPOS_PREFIX="/home/claw/kagenti/kagenti-extensions/"
13+
# Arg 2 is now a full "owner/name" reference, emitted verbatim in .repo.
14+
REPO="rossoctl/cortex"
15+
REPOS_PREFIX="/home/claw/kagenti/cortex/"
1516

1617
PASS=0
1718
FAIL=0
@@ -101,7 +102,7 @@ write_fixture "$TEST_TMPDIR/ext.json" \
101102
run_test "external record count" "$TEST_TMPDIR/ext.json" 'length' '1'
102103
run_test "external status normalized" "$TEST_TMPDIR/ext.json" '.[0].status' '404'
103104
run_test "external category" "$TEST_TMPDIR/ext.json" '.[0].category' 'external'
104-
run_test "external repo prefixed" "$TEST_TMPDIR/ext.json" '.[0].repo' 'kagenti/kagenti-extensions'
105+
run_test "external repo emitted verbatim" "$TEST_TMPDIR/ext.json" '.[0].repo' 'rossoctl/cortex'
105106
run_test "external file prefix stripped" "$TEST_TMPDIR/ext.json" '.[0].file' 'docs/d.md'
106107
run_test "external url preserved" "$TEST_TMPDIR/ext.json" '.[0].url' 'https://example.invalid/gone'
107108

0 commit comments

Comments
 (0)