Skip to content
Merged
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
224 changes: 166 additions & 58 deletions .github/bump-callers/bump-callers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -89,56 +89,134 @@ if (( ${#CALLERS[@]} == 0 )); then
exit 1
fi

# Bump a single caller repo. Every external call is guarded, so a bad repo
# returns non-zero here (caught by the loop) instead of killing the run. An
# expected skip (file missing / already pinned) is a successful `return 0`.
bump_one() {
local ENTRY="$1" REPO FILE LABEL FILE_ENC
REPO=$(cut -d'|' -f1 <<<"$ENTRY")
FILE=$(cut -d'|' -f2 <<<"$ENTRY")
LABEL=$(cut -d'|' -f3 <<<"$ENTRY")
FILE_ENC="${FILE//\//%2F}"
# Bump ONE caller repo, committing EVERY file that repo pins onto a single
# stable bump branch. Called once per repo with that repo's entries, so a repo
# listed more than once in the caller variable (a monorepo pinning the reusable
# workflow from several workflow files) lands ALL its files on ONE branch/PR.
# Every external call is guarded, so a bad repo returns non-zero here (caught by
# the loop) instead of killing the run. A repo whose files are all missing or
# already pinned commits nothing and is a successful skip (`return 0`).
#
# Grouping by repo is load-bearing: the branch is reset to the default-branch
# tip ONCE per repo, then each file is committed onto it. The previous per-entry
# loop reset the branch before EACH file, so a second same-repo file's reset
# discarded the first file's commit and the PR shipped only the last file — a
# silent partial bump (BE-3896).
bump_repo() {
local REPO="$1"; shift # remaining args: this repo's "repo|file|label" entries

local DEFAULT_BRANCH
DEFAULT_BRANCH=$(gh api "repos/${REPO}" --jq '.default_branch') || {
echo "::warning::${REPO}: cannot read repo metadata — skipping"
return 1
}

# Fetch current file; a missing file is an expected skip (success).
local CURRENT
CURRENT=$(gh api "repos/${REPO}/contents/${FILE_ENC}?ref=${DEFAULT_BRANCH}" 2>/dev/null) || {
echo "::warning::${REPO}: ${FILE} not found — skipping"
return 0
# Resolve the default-branch tip ONCE, up front, and pin every Pass-1 blob
# fetch to this immutable SHA (not the mutable `?ref=<branch>`). This closes a
# TOCTOU race: if the caller's default branch moved between the fetches and the
# reset below, a staged blob `sha` would be stale against the new tip and its
# Pass-2 PUT would 409 after earlier files already committed. Reading the tip
# is a GET (no branch churn), so it is safe before the all-skip early return.
local MAIN_SHA
MAIN_SHA=$(gh api "repos/${REPO}/git/refs/heads/${DEFAULT_BRANCH}" --jq '.object.sha') || {
echo "::warning::${REPO}: cannot read ${DEFAULT_BRANCH} ref — skipping"
return 1
}

local BLOB_SHA OLD_CONTENT
BLOB_SHA=$(jq -r '.sha' <<<"$CURRENT")
OLD_CONTENT=$(jq -r '.content' <<<"$CURRENT" | base64 -d)
# Pass 1 — stage every file that actually needs a bump WITHOUT writing
# anything yet. This keeps the reset/commit path off entirely for a repo whose
# files are all missing or already pinned (the old per-entry skips), so it
# never resets a branch or opens a PR it doesn't need.
local -a PEND_FILE_ENC=() PEND_CONTENT=() PEND_BLOB=() LABELS=()
local ENTRY FILE LABEL FILE_ENC CURRENT BLOB_SHA OLD_CONTENT NEW_CONTENT
for ENTRY in "$@"; do
FILE=$(cut -d'|' -f2 <<<"$ENTRY")
LABEL=$(cut -d'|' -f3 <<<"$ENTRY")
FILE_ENC="${FILE//\//%2F}"

# De-duplicate by file: a repo listed twice for the SAME path (a structurally
# valid config) must stage that file ONCE. A second PUT carrying the same
# pre-bump blob `sha` would 409 — the first PUT already moved the blob — and
# fail the whole repo even though the file was actually bumped. Fold the
# duplicate entry's label into the set (it still applies to the one PR) but
# do not re-stage the file.
local SEEN_FILE=0 P
for P in ${PEND_FILE_ENC[@]+"${PEND_FILE_ENC[@]}"}; do
[[ "$P" == "$FILE_ENC" ]] && { SEEN_FILE=1; break; }
done
if (( SEEN_FILE )); then
[[ -n "$LABEL" ]] && LABELS+=("$LABEL")
continue
fi

# Fetch current file, pinned to the resolved tip. Distinguish a genuine 404
# (this file is actually absent → an expected per-file skip) from ANY other
# failure (auth, rate limit, 5xx, network). Treating a transient error as
# "not found" and continuing would open a PR that silently OMITS this file
# while the job still reports success — the partial-bump class this refactor
# exists to kill (BE-3896). So a non-404 failure fails the whole repo.
local ERRFILE RC
ERRFILE=$(mktemp)
CURRENT=$(gh api "repos/${REPO}/contents/${FILE_ENC}?ref=${MAIN_SHA}" 2>"$ERRFILE"); RC=$?
if (( RC != 0 )); then
if grep -qi 'HTTP 404' "$ERRFILE"; then
echo "::warning::${REPO}: ${FILE} not found — skipping"
rm -f "$ERRFILE"
continue
fi
echo "::warning::${REPO}: ${FILE} fetch failed (HTTP error, not 404) — failing repo to avoid a partial bump"
rm -f "$ERRFILE"
return 1
fi
rm -f "$ERRFILE"

BLOB_SHA=$(jq -r '.sha' <<<"$CURRENT")
OLD_CONTENT=$(jq -r '.content' <<<"$CURRENT" | base64 -d)

# Rewrite the github-workflows pin(s) to NEW_SHA and normalize the stale
# `# github-workflows#NN` pin comment. Anchor the 40-hex substitution to the
# two known pin contexts — the `uses: …Comfy-Org/github-workflows…@<sha>`
# line and agents-md-integrity's bare `workflows_ref: <sha>` line — so a
# full-SHA pin of ANOTHER action in the same file (`actions/checkout@<sha>`,
# the org's mandated practice) is never clobbered to github-workflows' SHA.
# The comment rewrite is a no-op for callers that use a different comment
# form (e.g. agents-md-integrity's `# v1`), so it is safe to share.
NEW_CONTENT=$(sed -E "/github-workflows|workflows_ref/ s/[0-9a-f]{40}/${NEW_SHA}/g; s|# github-workflows#[0-9]+|# github-workflows main (${SHORT})|g" <<<"$OLD_CONTENT")

# Already fully pinned → the rewrite is a no-op → nothing to do for this file.
# Comparing the rewritten content to the original (rather than grepping for
# NEW_SHA appearing *anywhere*) also repairs a half-bumped file: if a prior
# run left one of two refs at NEW_SHA and the other stale, the content still
# differs here, so the file is re-staged and repaired instead of skipped.
if [[ "$NEW_CONTENT" == "$OLD_CONTENT" ]]; then
echo "${REPO}: ${FILE} already at ${SHORT} — skipping"
continue
fi

# Collect the label ONLY now that the file is confirmed staged. Labels from
# entries that were skipped (missing / already-pinned) must not land on the
# repo's real bump PR — a stray blocking label (e.g. `do-not-merge`) on a
# skipped entry would else be applied to the actual bump.
[[ -n "$LABEL" ]] && LABELS+=("$LABEL")

# `$(...)` capture above (base64 -d, then sed) strips the file's trailing
# newline, so printf '%s\n' restores exactly one — otherwise the committed
# caller loses its EOF newline and trips the receiver repo's formatter
# (oxfmt/prettier both require a trailing newline).
PEND_FILE_ENC+=("$FILE_ENC")
PEND_CONTENT+=("$(printf '%s\n' "$NEW_CONTENT" | base64 | tr -d '\n')")
PEND_BLOB+=("$BLOB_SHA")
done

# Already pinned to this SHA → nothing to do (success).
if grep -qF "$NEW_SHA" <<<"$OLD_CONTENT"; then
echo "${REPO}: already at ${SHORT} — skipping"
# Nothing to bump for this repo → clean skip: no branch churn, no PR.
if (( ${#PEND_FILE_ENC[@]} == 0 )); then
return 0
fi

# Replace every 40-char hex SHA (the caller's pin — cursor-review has one,
# agents-md-integrity has two: the `uses: ...@<sha>` and its `workflows_ref`)
# and normalize the stale `# github-workflows#NN` pin comment. The comment
# rewrite is a no-op for callers that use a different comment form (e.g.
# agents-md-integrity's `# v1`), so it is safe to share across fleets.
local NEW_CONTENT
NEW_CONTENT=$(sed -E "s/[0-9a-f]{40}/${NEW_SHA}/g; s|# github-workflows#[0-9]+|# github-workflows main (${SHORT})|g" <<<"$OLD_CONTENT")

# Rebuild the stable bump branch from the caller's CURRENT default-branch tip
# every run, so the PR is always a clean single-commit "bump to @SHORT" diff —
# it never accumulates stale commits across bumps and never drifts if the
# caller's default branch moved since the last bump (BE-3882).
local MAIN_SHA
MAIN_SHA=$(gh api "repos/${REPO}/git/refs/heads/${DEFAULT_BRANCH}" --jq '.object.sha') || {
echo "::warning::${REPO}: cannot read ${DEFAULT_BRANCH} ref — skipping"
return 1
}
# Rebuild the stable bump branch from the resolved default-branch tip (MAIN_SHA,
# captured up front) ONCE for the repo, so the PR is always a clean "bump to
# @SHORT" diff — it never accumulates stale commits across bumps and never
# drifts if the caller's default branch moved since the last bump (BE-3882).
# Create the branch at the tip; if it already exists (an open bump PR, or
# residue from a prior run) force it back to the tip so the diff is rebuilt
# from scratch rather than committed on top of the old bump.
Expand All @@ -152,23 +230,22 @@ bump_one() {
}
fi

# Commit the updated file as the single commit on top of the tip. `$(...)`
# capture above (base64 -d, then sed) strips the file's trailing newline, so
# printf '%s\n' restores exactly one — otherwise the committed caller loses its
# EOF newline and trips the receiver repo's formatter (oxfmt/prettier both
# require a trailing newline). BLOB_SHA is the file's blob at the tip we just
# reset to, so the update applies cleanly regardless of any prior branch state.
local ENCODED
ENCODED=$(printf '%s\n' "$NEW_CONTENT" | base64 | tr -d '\n')
gh api --method PUT "repos/${REPO}/contents/${FILE_ENC}" \
--field message="ci: bump ${TAG} to github-workflows@${SHORT}" \
--field content="${ENCODED}" \
--field sha="${BLOB_SHA}" \
--field branch="${BRANCH}" \
> /dev/null || {
echo "::warning::${REPO}: commit to ${BRANCH} failed — skipping"
return 1
}
# Pass 2 — commit each staged file onto the (single) branch in turn. Each PUT
# carries that file's own blob SHA from the tip; because the files are
# distinct, committing one never changes another's blob, so every PUT applies
# cleanly on top of the previous one and the branch accumulates ALL of them.
local i
for (( i = 0; i < ${#PEND_FILE_ENC[@]}; i++ )); do
gh api --method PUT "repos/${REPO}/contents/${PEND_FILE_ENC[$i]}" \
--field message="ci: bump ${TAG} to github-workflows@${SHORT}" \
--field content="${PEND_CONTENT[$i]}" \
--field sha="${PEND_BLOB[$i]}" \
Comment thread
mattmillerai marked this conversation as resolved.
--field branch="${BRANCH}" \
> /dev/null || {
echo "::warning::${REPO}: commit to ${BRANCH} failed — skipping"
return 1
Comment thread
mattmillerai marked this conversation as resolved.
}
done

# The title/body embed ${SHORT}. Build them once so the create and the
# update-in-place paths post the identical, current text. The body is a single
Expand Down Expand Up @@ -207,8 +284,21 @@ bump_one() {

# No open PR for this branch → open one (first bump, or the prior PR was
# merged/closed — and its branch possibly auto-deleted — since the last run).
local LABEL_ARGS=()
[[ -n "$LABEL" ]] && LABEL_ARGS=(--label "$LABEL")
# Apply every distinct non-empty label the repo's entries carried (they need
# not agree; dedup so `gh pr create` isn't passed the same label twice).
# Exact-match membership (not a `|${L}|` substring sentinel): GitHub label
# names may themselves contain `|`, so a substring test could drop a distinct
# label whose name is a substring of an already-seen one (e.g. `bug` vs `bug|ui`).
local -a LABEL_ARGS=() SEEN_LABELS=()
local L S DUP
for L in ${LABELS[@]+"${LABELS[@]}"}; do
DUP=0
for S in ${SEEN_LABELS[@]+"${SEEN_LABELS[@]}"}; do
[[ "$S" == "$L" ]] && { DUP=1; break; }
done
(( DUP )) && continue
SEEN_LABELS+=("$L"); LABEL_ARGS+=(--label "$L")
done
if gh pr create \
--repo "${REPO}" \
--head "${BRANCH}" \
Expand All @@ -229,9 +319,27 @@ bump_one() {
fi
}

FAILED=()
# Group the flat entry list by repo, preserving first-seen order, so each repo
# is bumped exactly once with ALL of its files (BE-3896). `|` can't appear in a
# repo name (it is the field delimiter), so it is a safe membership sentinel.
REPOS=()
SEEN_REPOS=""
for ENTRY in "${CALLERS[@]}"; do
bump_one "$ENTRY" || FAILED+=("${ENTRY%%|*}")
REPO="${ENTRY%%|*}"
case "$SEEN_REPOS" in
*"|${REPO}|"*) ;;
*) REPOS+=("$REPO"); SEEN_REPOS="${SEEN_REPOS}|${REPO}|" ;;
esac
done

FAILED=()
for REPO in "${REPOS[@]}"; do
# Collect this repo's entries (in their original order) and bump them together.
ENTRIES=()
for ENTRY in "${CALLERS[@]}"; do
[[ "${ENTRY%%|*}" == "$REPO" ]] && ENTRIES+=("$ENTRY")
done
bump_repo "$REPO" "${ENTRIES[@]}" || FAILED+=("$REPO")
done

if (( ${#FAILED[@]} )); then
Expand Down
Loading