Skip to content

Commit d6ee033

Browse files
Merge branch 'main' into fix/restore-fixer-and-clear-path-scars
2 parents 4f5a3bf + 9e6abd2 commit d6ee033

2 files changed

Lines changed: 156 additions & 14 deletions

File tree

‎scripts/propagate-sha-bump.sh‎

Lines changed: 116 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
# Optional env:
2727
# FARM_REPO default "hyperpolymath/.git-private-farm"
2828
# DRY_RUN "true" prints the payload without dispatching
29+
# GH_BIN GitHub CLI command (test seam; default "gh")
2930
set -euo pipefail
3031

3132
usage() {
@@ -40,6 +41,7 @@ FINDING_FILE="$2"
4041

4142
FARM_REPO="${FARM_REPO:-hyperpolymath/.git-private-farm}"
4243
DRY_RUN="${DRY_RUN:-false}"
44+
GH_BIN="${GH_BIN:-gh}"
4345

4446
# Title-keyword exclusion regex. Keep in sync with:
4547
# feedback_pr_sweep_title_keyword_exclusion
@@ -88,6 +90,45 @@ case "$source_workflow" in
8890
*) echo "ERROR: source_workflow not in expected shape: '$source_workflow'" >&2; exit 1 ;;
8991
esac
9092

93+
# A SHA can exist in GitHub's object database yet be unusable as a cross-repo
94+
# reusable workflow. Feature-branch commits that are later squash-merged have
95+
# exactly that shape: Contents/Commits APIs resolve them, but Actions rejects
96+
# `uses: ...@sha` at startup with `workflow was not found`. Prove the proposed
97+
# target is on the source repository's default-branch history before searching
98+
# consumers or dispatching a mutation.
99+
default_branch=$($GH_BIN api "repos/${source_repo}" --jq '.default_branch') || {
100+
echo "ERROR: could not resolve ${source_repo}'s default branch" >&2
101+
exit 1
102+
}
103+
104+
compare_status=$($GH_BIN api \
105+
"repos/${source_repo}/compare/${new_sha}...${default_branch}" \
106+
--jq '.status') || {
107+
echo "ERROR: could not verify ${new_sha} against ${source_repo}:${default_branch}" >&2
108+
exit 1
109+
}
110+
111+
case "$compare_status" in
112+
identical|ahead) ;;
113+
*)
114+
echo "ERROR: new_sha ${new_sha} exists but is not reachable from ${source_repo}:${default_branch} (compare status: ${compare_status:-unknown})" >&2
115+
exit 1
116+
;;
117+
esac
118+
119+
if [[ "$source_workflow" == .github/workflows/* ]]; then
120+
source_type=$($GH_BIN api \
121+
"repos/${source_repo}/contents/${source_workflow}?ref=${new_sha}" \
122+
--jq '.type') || {
123+
echo "ERROR: ${source_workflow} does not resolve at ${source_repo}@${new_sha}" >&2
124+
exit 1
125+
}
126+
[[ "$source_type" == "file" ]] || {
127+
echo "ERROR: ${source_workflow} at ${source_repo}@${new_sha} is not a file" >&2
128+
exit 1
129+
}
130+
fi
131+
91132
# --- 3. Title-keyword pre-filter (HARD) ---------------------------------------
92133

93134
# Per feedback_no_automated_licence_edits: licence/SPDX changes are MANUAL,
@@ -119,41 +160,102 @@ CONSUMERS_TSV="$TMPDIR_RUN/consumers.tsv"
119160

120161
echo "Enumerating consumers pinning: $needle" >&2
121162

122-
# gh code-search has a 100-result cap per query. For larger sweeps the
123-
# operator should pre-build a TSV manually and supply it via a CONSUMERS_TSV
124-
# env override. Tracked here for posterity.
163+
# Search both named estates, page to the authoritative `total_count`, and fail
164+
# rather than silently truncate if GitHub's 1,000-result search horizon is ever
165+
# reached. The old `gh search code --limit 100 --owner hyperpolymath` path both
166+
# capped its answer and omitted metadatastician entirely.
125167
if [[ -n "${CONSUMERS_TSV_OVERRIDE:-}" && -f "$CONSUMERS_TSV_OVERRIDE" ]]; then
126168
cp "$CONSUMERS_TSV_OVERRIDE" "$CONSUMERS_TSV"
127169
echo "Using override consumers TSV: $CONSUMERS_TSV_OVERRIDE" >&2
128170
else
129-
gh search code "$needle" --owner hyperpolymath --limit 100 \
130-
--json repository,path \
131-
--jq '.[] | select(.path | startswith(".github/workflows/")) | "\(.repository.nameWithOwner)\t\(.path)"' \
132-
> "$CONSUMERS_TSV" || true
171+
: > "$CONSUMERS_TSV"
172+
173+
# Paginate GitHub code-search for a given scope (user:… or org:…), validate
174+
# result integrity, and append workflow YAML paths to CONSUMERS_TSV. Fails
175+
# if total_count exceeds GitHub's 1,000-result horizon or if the API returns
176+
# incomplete_results=true.
177+
#
178+
# Args:
179+
# $1 scope qualifier (e.g. "user:hyperpolymath" or "org:metadatastician")
180+
# Env:
181+
# needle search pattern (reusable path + old SHA)
182+
# GH_BIN GitHub CLI command
183+
# CONSUMERS_TSV output file (appended)
184+
# Returns:
185+
# 0 on success, 1 on search failure or integrity violation
186+
search_scope() {
187+
local scope="$1" page=1 body total incomplete count
188+
while :; do
189+
body=$($GH_BIN api -X GET search/code \
190+
-f "q=${needle} ${scope}" \
191+
-F per_page=100 \
192+
-F "page=${page}") || {
193+
echo "ERROR: code search failed for scope '${scope}' page ${page}" >&2
194+
return 1
195+
}
196+
197+
total=$(jq -r '.total_count' <<<"$body")
198+
incomplete=$(jq -r '.incomplete_results' <<<"$body")
199+
[[ "$total" =~ ^[0-9]+$ && "$incomplete" == false ]] || {
200+
echo "ERROR: invalid/incomplete code-search result for '${scope}'" >&2
201+
return 1
202+
}
203+
(( total <= 1000 )) || {
204+
echo "ERROR: '${scope}' has ${total} matches, beyond GitHub's 1,000-result search horizon; split the query before propagating" >&2
205+
return 1
206+
}
207+
208+
jq -r '.items[]
209+
| select(.path | startswith(".github/workflows/"))
210+
| select(.path | test("\\.ya?ml$"))
211+
| "\(.repository.full_name)\t\(.path)"' <<<"$body" >> "$CONSUMERS_TSV"
212+
213+
count=$(jq '.items | length' <<<"$body")
214+
(( count == 100 && page * 100 < total )) || break
215+
page=$((page + 1))
216+
done
217+
}
218+
219+
search_scope "user:hyperpolymath"
220+
search_scope "org:metadatastician"
221+
sort -u -o "$CONSUMERS_TSV" "$CONSUMERS_TSV"
133222
fi
134223

135224
# Drop fork repos — per estate license policy, third-party / forked stuff is
136225
# off-limits. (gh search code does not filter forks; we look up each owner-repo
137226
# pair and skip forks.) For large sweeps this round-trips N times — cache as
138227
# needed.
228+
#
229+
# Filters a TSV of repo + workflow path pairs, removing forks, archived repos,
230+
# and inaccessible repositories. Overwrites the input file in-place with the
231+
# filtered result.
232+
#
233+
# Args:
234+
# $1 path to TSV file (format: "owner/repo<TAB>workflow_path")
235+
# Env:
236+
# GH_BIN GitHub CLI command
237+
# Side effects:
238+
# Overwrites the input TSV with filtered content (non-fork, non-archived,
239+
# accessible repos only). Logs skipped repos to stderr.
139240
filter_forks() {
140241
local tsv="$1"
141242
local out="${tsv}.no-forks"
142243
: > "$out"
143244
while IFS=$'\t' read -r repo path; do
144-
local is_fork
145-
is_fork=$(gh repo view "$repo" --json isFork --jq '.isFork' 2>/dev/null || echo "true")
146-
if [[ "$is_fork" == "false" ]]; then
245+
local repo_state
246+
repo_state=$($GH_BIN api "repos/${repo}" --jq '[.fork, .archived] | @tsv' 2>/dev/null || printf 'true\ttrue\n')
247+
if [[ "$repo_state" == $'false\tfalse' ]]; then
147248
printf '%s\t%s\n' "$repo" "$path" >> "$out"
148249
else
149-
echo "SKIP (fork): $repo" >&2
250+
echo "SKIP (fork, archived, or inaccessible): $repo" >&2
150251
fi
151252
done < "$tsv"
152253
mv "$out" "$tsv"
153254
}
154255

155-
# Skip fork-filter if the operator supplied an override TSV — they've already vetted it.
156-
if [[ -s "$CONSUMERS_TSV" && -z "${CONSUMERS_TSV_OVERRIDE:-}" ]]; then
256+
# Apply the same active-root eligibility check to searched and overridden
257+
# consumers so the test seam cannot bypass the production safety boundary.
258+
if [[ -s "$CONSUMERS_TSV" ]]; then
157259
filter_forks "$CONSUMERS_TSV"
158260
fi
159261

@@ -221,6 +323,6 @@ fi
221323
echo "Firing repository_dispatch propagate-sha-bump → $FARM_REPO ($n_consumers consumers)" >&2
222324

223325
printf '%s' "$payload" \
224-
| gh api -X POST "repos/${FARM_REPO}/dispatches" --input -
326+
| "$GH_BIN" api -X POST "repos/${FARM_REPO}/dispatches" --input -
225327

226328
echo "OK: dispatch fired. Receiver workflow will run async on $FARM_REPO." >&2

‎tests/propagate-sha-bump-smoke.sh‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,23 @@ SCRIPT="scripts/propagate-sha-bump.sh"
1515
tmp=$(mktemp -d)
1616
trap 'rm -rf "$tmp"' EXIT
1717

18+
# Hermetic GitHub seam. The actuator must prove the target is reachable and
19+
# that the reusable exists before it considers consumers; these responses model
20+
# a mainline target without touching the network.
21+
cat > "$tmp/gh" <<'EOF'
22+
#!/usr/bin/env bash
23+
set -euo pipefail
24+
case "$*" in
25+
*'/compare/'*) printf '%s\n' "${MOCK_COMPARE_STATUS:-ahead}" ;;
26+
*'/contents/'*) printf '%s\n' file ;;
27+
*'repos/hyperpolymath/standards --jq .default_branch'*) printf '%s\n' main ;;
28+
*'--jq [.fork, .archived] | @tsv'*) printf 'false\tfalse\n' ;;
29+
*) printf '%s\n' '{}' ;;
30+
esac
31+
EOF
32+
chmod +x "$tmp/gh"
33+
export GH_BIN="$tmp/gh"
34+
1835
pass=0
1936
fail=0
2037

@@ -147,6 +164,14 @@ rc=$?
147164
set -e
148165
assert_exit "DRY_RUN with valid finding → exit 0" 0 "$rc"
149166

167+
if printf '%s' "$out" | grep -q 'hyperpolymath/repo-a'; then
168+
echo "ok active override consumer remains eligible"
169+
((pass++))
170+
else
171+
echo "FAIL active override consumers were filtered out"
172+
((fail++))
173+
fi
174+
150175
if printf '%s' "$out" | grep -q '"event_type": "propagate-sha-bump"'; then
151176
echo "ok payload contains event_type"
152177
((pass++))
@@ -173,6 +198,21 @@ else
173198
((fail++))
174199
fi
175200

201+
# 7. Existing but non-mainline new_sha → hard refusal before fan-out.
202+
set +e
203+
out=$(MOCK_COMPARE_STATUS=diverged CONSUMERS_TSV_OVERRIDE="$tmp/consumers.tsv" \
204+
DRY_RUN=true "$SCRIPT" /ignored "$tmp/good.json" 2>&1)
205+
rc=$?
206+
set -e
207+
assert_exit "diverged reusable target → exit 1" 1 "$rc"
208+
if printf '%s' "$out" | grep -q 'not reachable'; then
209+
echo "ok non-mainline refusal explains reachability"
210+
((pass++))
211+
else
212+
echo "FAIL non-mainline refusal message missing"
213+
((fail++))
214+
fi
215+
176216
echo ""
177217
echo "passed: $pass failed: $fail"
178218
[[ "$fail" -eq 0 ]]

0 commit comments

Comments
 (0)