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
17 changes: 15 additions & 2 deletions .github/scripts/replace-template-sync-commit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,21 @@ jq -e \
and .commit.verification.verified == true' \
<<<"$verified_commit" >/dev/null || fail "replacement commit readback changed content or lost verification"

updated_ref="$(gh api "$ref_endpoint")" || fail "could not read back the generated branch"
updated_sha="$(jq -er '.object.sha' <<<"$updated_ref")" || fail "updated branch response has no sha"
# GitHub can serve a branch's previous commit for a moment after an update. Read it back a few
# times before failing, and still fail closed when it keeps pointing elsewhere (#897).
readback_attempts=5
readback_pause_seconds=2
updated_sha=""
for ((readback = 1; readback <= readback_attempts; readback++)); do
updated_ref="$(gh api "$ref_endpoint")" || fail "could not read back the generated branch"
updated_sha="$(jq -er '.object.sha' <<<"$updated_ref")" || fail "updated branch response has no sha"
if [[ "$updated_sha" == "$signed_sha" ]]; then
break
fi
if ((readback < readback_attempts)); then
sleep "$readback_pause_seconds"
fi
done
[[ "$updated_sha" == "$signed_sha" ]] || fail "generated branch does not point to the verified replacement"

printf '%s\n' "$signed_sha"
55 changes: 54 additions & 1 deletion .github/tests/test-template-sync-signed-commit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,17 @@ printf '%s %s\n' "$method" "$endpoint" >>"$FAKE_GH_LOG"
case "$method $endpoint" in
"GET repos/example/consumer/git/ref/heads/chore/template-sync_deadbee")
if [[ -f "$FAKE_GH_UPDATED" ]]; then
jq -n --arg sha "$SIGNED_SHA" '{object:{sha:$sha}}'
# After the update, serve the previous commit for the first FAKE_GH_STALE_READS reads.
stale_seen=0
if [[ -f "$FAKE_GH_UPDATED.reads" ]]; then
stale_seen="$(cat "$FAKE_GH_UPDATED.reads")"
fi
if ((stale_seen < ${FAKE_GH_STALE_READS:-0})); then
printf '%s\n' "$((stale_seen + 1))" >"$FAKE_GH_UPDATED.reads"
jq -n --arg sha "$REMOTE_SHA" '{object:{sha:$sha}}'
else
jq -n --arg sha "$SIGNED_SHA" '{object:{sha:$sha}}'
fi
else
jq -n --arg sha "$REMOTE_SHA" '{object:{sha:$sha}}'
fi
Expand Down Expand Up @@ -175,6 +185,49 @@ fi
[[ ! -f "$FAKE_GH_UPDATED" ]] || fail "helper moved the ref before signature verification passed"
grep -q "GitHub did not verify" "$fixture/error" || fail "unsigned failure did not name the verification gate"

# A branch read that is stale once after the update, then current, still succeeds (#897).
run_readback_case() { # <name> <stale reads> -> runs the helper; output and error land in the fixture
fixture="$(make_fixture "$1")"
base_sha="$(git -C "$fixture/repo" rev-parse HEAD)"
git -C "$fixture/repo" switch -q -c chore/template-sync_deadbee
printf 'synced\n' >"$fixture/repo/file.txt"
git -C "$fixture/repo" add file.txt
git -C "$fixture/repo" commit -q -m "chore: sync template"
EXPECTED_TREE="$(git -C "$fixture/repo" rev-parse 'HEAD^{tree}')"
export EXPECTED_TREE
export EXPECTED_PARENT="$base_sha"
EXPECTED_MESSAGE="$(git -C "$fixture/repo" log -1 --format=%B | sed '${/^$/d;}')"
export EXPECTED_MESSAGE
REMOTE_SHA="$(git -C "$fixture/repo" rev-parse HEAD)"
export REMOTE_SHA
export FAKE_GH_LOG="$fixture/gh.log"
export FAKE_GH_UPDATED="$fixture/updated"
export FAKE_GH_MODE=success
export FAKE_GH_STALE_READS="$2"
(
cd "$fixture/repo"
PATH="$fixture/bin:$PATH" GITHUB_REPOSITORY=example/consumer \
"$helper" --base-sha "$base_sha" --branch-prefix chore/template-sync
) >"$fixture/output" 2>"$fixture/error"
}
# Count the branch reads the helper made after it moved the branch.
reads_after_update() {
awk '/^PATCH / { seen = 1; next } seen && /^GET repos\/example\/consumer\/git\/ref\/heads\// { n++ } END { print n + 0 }' "$FAKE_GH_LOG"
}

run_readback_case stale-once 1 || fail "helper failed although the branch read was stale only once"
grep -qx "$SIGNED_SHA" "$fixture/output" || fail "stale-once readback did not return the verified replacement sha"
[[ "$(reads_after_update)" == 2 ]] || fail "stale-once readback did not read the branch exactly twice"

# A branch that keeps pointing at another commit still fails, after the bounded retries.
if run_readback_case never-current 99; then
fail "helper accepted a branch that never pointed at the verified replacement"
fi
grep -q "generated branch does not point to the verified replacement" "$fixture/error" ||
fail "persistent readback mismatch did not name the branch gate"
[[ "$(reads_after_update)" == 5 ]] || fail "persistent readback mismatch was not read exactly five times"
unset FAKE_GH_STALE_READS

fixture="$(make_fixture no-change)"
base_sha="$(git -C "$fixture/repo" rev-parse HEAD)"
export FAKE_GH_LOG="$fixture/gh.log"
Expand Down
Loading