Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 72 additions & 7 deletions scripts/propagate-workflow-pins.sh
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@
# PATH may be a single consumer repo (has .github/workflows) or a parent
# directory of many repos. Defaults to the current directory.
# --to <sha> target standards SHA to pin to. If omitted, resolved from
# (in order) $STANDARDS_TARGET_SHA, a local standards checkout
# ($STANDARDS_DIR or ~/standards), then `git ls-remote` HEAD.
# (in order) $STANDARDS_TARGET_SHA, the main ref of a local
# standards checkout ($STANDARDS_DIR or ~/standards), then the
# remote main ref. A feature-worktree HEAD is never selected.
#
# Output: tab-separated audit lines suitable for review.

Expand Down Expand Up @@ -71,19 +72,28 @@ log() { printf '%s\n' "$*" >&2; }

# ---------------------------------------------------------------------------
# Resolve the target SHA
# ---------------------------------------------------------------------------
# resolve_target resolves the standards commit SHA from an explicit target, a local main ref, or the remote main ref.

resolve_target() {
if [ -n "$TARGET_SHA" ]; then printf '%s' "$TARGET_SHA"; return 0; fi

local sd="${STANDARDS_DIR:-$HOME/standards}"
if git -C "$sd" rev-parse HEAD >/dev/null 2>&1; then
git -C "$sd" rev-parse HEAD; return 0
fi
# The old implementation used HEAD. When invoked from a standards feature
# worktree, that propagated a PR-intermediate commit before it had landed on
# main. GitHub's contents API could read that commit, but cross-repository
# reusable workflows rejected it as `workflow was not found` (251 active
# workflow files / 70 repos in the 2026-09-04 incident).
local ref
for ref in refs/remotes/origin/main refs/heads/main; do
if git -C "$sd" rev-parse --verify "${ref}^{commit}" >/dev/null 2>&1; then
git -C "$sd" rev-parse "${ref}^{commit}"
return 0
fi
done

# Network fallback — the live standards HEAD.
local remote
remote=$(git ls-remote https://github.com/hyperpolymath/standards.git HEAD 2>/dev/null | awk '{print $1}')
remote=$(git ls-remote https://github.com/hyperpolymath/standards.git refs/heads/main 2>/dev/null | awk '{print $1}')
if [ -n "$remote" ]; then printf '%s' "$remote"; return 0; fi

return 1
Expand All @@ -94,6 +104,61 @@ if ! TARGET_SHA="$(resolve_target)"; then
exit 2
fi

# Prove that the proposed pin is a full commit SHA reachable from standards'
# default branch. Merely proving that `/commits/<sha>` exists is insufficient:
# a squash-merged PR leaves its intermediate commits addressable through the
# validate_target verifies that TARGET_SHA is a full commit SHA reachable from the standards repository's main branch.
validate_target() {
[[ "$TARGET_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || {
log "ERROR: target must be a full 40-character hexadecimal commit SHA: $TARGET_SHA"
return 1
}

local sd="${STANDARDS_DIR:-$HOME/standards}" ref head gd
if git -C "$sd" cat-file -e "${TARGET_SHA}^{commit}" >/dev/null 2>&1; then
for ref in refs/remotes/origin/main refs/heads/main; do
if head=$(git -C "$sd" rev-parse --verify "${ref}^{commit}" 2>/dev/null); then
if git -C "$sd" merge-base --is-ancestor "$TARGET_SHA" "$head"; then
return 0
fi

# A complete local graph gives a conclusive negative. A shallow or
# partial clone does not; let the server settle that case below.
gd=$(git -C "$sd" rev-parse --absolute-git-dir 2>/dev/null || true)
if [ -n "$gd" ] && [ ! -e "$gd/shallow" ] &&
[ "$(git -C "$sd" config --get remote.origin.promisor 2>/dev/null || true)" != true ] &&
[ -z "$(git -C "$sd" config --get remote.origin.partialclonefilter 2>/dev/null || true)" ]; then
log "ERROR: target ${TARGET_SHA} exists but is not reachable from local standards main (${head})."
return 1
fi
break
fi
done
fi

local api="${STANDARDS_REACHABILITY_API_BASE:-https://api.github.com}"
local -a auth=()
[ -n "${GITHUB_TOKEN:-}" ] && auth=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
local body status
body=$(curl -fsS --max-time 20 \
-H 'Accept: application/vnd.github+json' \
"${auth[@]}" \
"$api/repos/hyperpolymath/standards/compare/${TARGET_SHA}...main") || {
log "ERROR: could not prove target ${TARGET_SHA} is reachable from standards main; refusing propagation."
return 1
}
status=$(printf '%s' "$body" | sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([a-z]*\)".*/\1/p' | head -n1)
case "$status" in
identical|ahead) return 0 ;;
*)
log "ERROR: target ${TARGET_SHA} is not reachable from standards main (compare status: ${status:-unknown}); refusing propagation."
return 1
;;
esac
}

validate_target || exit 2

# ---------------------------------------------------------------------------
# Per-file rewrite (the unit-testable core)
# ---------------------------------------------------------------------------
Expand Down
30 changes: 29 additions & 1 deletion scripts/tests/propagate-workflow-pins-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,25 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROP="$SCRIPT_DIR/../propagate-workflow-pins.sh"

OLD="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
TARGET="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"

# A real local commit graph makes reachability part of every test. The
# propagation primitive must reject a SHA that merely exists on a feature
# branch: GitHub cannot resolve such a SHA as a cross-repo reusable workflow.
UPSTREAM="$TEST_DIR/standards"
git init -q -b main "$UPSTREAM"
git -C "$UPSTREAM" config user.email t@t
git -C "$UPSTREAM" config user.name t
touch "$UPSTREAM/reusable.yml"
git -C "$UPSTREAM" add reusable.yml
git -C "$UPSTREAM" commit -q -m main
TARGET=$(git -C "$UPSTREAM" rev-parse HEAD)
git -C "$UPSTREAM" checkout -q -b feature
touch "$UPSTREAM/feature-only"
git -C "$UPSTREAM" add feature-only
git -C "$UPSTREAM" commit -q -m feature
ORPHAN=$(git -C "$UPSTREAM" rev-parse HEAD)
git -C "$UPSTREAM" checkout -q main
export STANDARDS_DIR="$UPSTREAM"

PASS=0; TOTAL=0

Expand Down Expand Up @@ -90,6 +108,16 @@ OUT=$(bash "$PROP" --to "$TARGET" "$ROOT")
try "parent-dir mode sees repoA" contains repoA "$OUT"
try "parent-dir mode sees repoB" contains repoB "$OUT"

# ── 8. Existing but non-mainline target is refused before any rewrite ───────
R="$TEST_DIR/orphan"; mk_consumer "$R"
set +e
OUT=$(bash "$PROP" --fix --to "$ORPHAN" "$R" 2>&1)
RC=$?
set -e
try "feature-only target is rejected" test "$RC" -eq 2
try "rejected target leaves consumer unchanged" file_has "$R/.github/workflows/governance.yml" "governance-reusable.yml@${OLD}"
try "rejection explains default-branch reachability" contains "not reachable" "$OUT"

echo "----------------------------------------"
echo "$PASS/$TOTAL test cases passed."
[ "$PASS" -eq "$TOTAL" ] || { echo "Some propagation tests FAILED."; exit 1; }
Expand Down
Loading