From e52dd0c3ab1cb36aaf8ac02a9f9621b609791364 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 16:12:33 +0200 Subject: [PATCH] fix(resolve_base): name merge_group.base_sha and pull_request.base.sha so a REBASE-replayed PR is judged whole (Refs #5855, #5859) Squash of #5859's two commits (2dc1928478, e3a089b2ec), same tree. The pull_request fallback reads base.sha past the nested repo/user objects of a real payload. Co-Authored-By: Claude Sonnet 5.5 --- scripts/check_roadmap_diff_additive.sh | 31 +++++++++++++++++++++++++- scripts/lib/resolve_base.sh | 24 ++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/scripts/check_roadmap_diff_additive.sh b/scripts/check_roadmap_diff_additive.sh index 07b1614084..38a0658346 100755 --- a/scripts/check_roadmap_diff_additive.sh +++ b/scripts/check_roadmap_diff_additive.sh @@ -92,7 +92,7 @@ if [ "${1:-}" = "--self-test" ]; then not_measured=0 # The case table has exactly this many rows; a run that reaches fewer, or more, is RED, never "n/n" # (#5047). Raise it with every row added. The two summary rows at the end are not counted. - SELFTEST_MIN_ROWS=27 + SELFTEST_MIN_ROWS=30 # selftest_verdict ROWS FAILS NOT_MEASURED MIN -> prints the summary; rc 0 all rows ran and # passed, 1 a row failed or other than MIN ran, 2 every row that ran passed but one was not measured. @@ -443,7 +443,36 @@ EOF *"is not a merge commit nor a commit on the origin/main tip"*) printf 'ok row %-2s stacked entry with deepening disabled (mutation): refused by name\n' "$row" ;; *) printf 'FAIL row %-2s stacked entry with deepening disabled: refused for the wrong reason (rc=%s): %s\n' "$row" "$rc7" "$err7"; fails=1 ;; esac + # Rows: a REBASE-replayed 3-commit pull request (#5855). The queue replays the PR as c2,c3,c4 on the + # main tip c1; the head's single parent is c3, so every parent rule above names c3 and judges ONLY the + # last commit. The event names the real base (merge_group.base_sha = c1); with the event base ignored + # (the mutation) the stacked rule answers c3, which is the defect. + row=$((row + 1)) + QR="$TD/rebase-src"; rm -rf "${QR:?}"; ( git init -q -b main "$QR" && cd "$QR" && git config user.email t@t && git config user.name t \ + && cp "$TD/base.yaml" r.yaml && git add r.yaml && git commit -qm c1 && git branch -q queue-base \ + && printf 'a\n' > a.txt && git add a.txt && git commit -qm c2 \ + && printf 'b\n' > b.txt && git add b.txt && git commit -qm c3 \ + && printf 'c\n' > c.txt && git add c.txt && git commit -qm c4 ) 2>/dev/null + rm -rf "${QR:?}.clone"; git clone -q --depth=1 -b main "file://$QR" "$QR.clone" 2>/dev/null; git -C "$QR.clone" fetch -q --depth=1 origin '+queue-base:refs/remotes/origin/main' 2>/dev/null + git -C "$QR" config uploadpack.allowReachableSHA1InWant true + wantr=$( git -C "$QR" rev-parse queue-base ) + printf '{"merge_group":{"base_sha":"%s"}}\n' "$wantr" > "$TD/rebase-event.json" + gotr=$( GITHUB_EVENT_NAME=merge_group GITHUB_EVENT_PATH="$TD/rebase-event.json" bash -c 'cd "$2" || exit 2; . "$0" --lib-only; REPO_ROOT="$1"; resolve_base HEAD && printf "%s|%s" "$BASE_REF" "$BASE_HOW"' "$SELF" "$QR.clone" "$QR.clone" 2>/dev/null ) || true + case "$gotr" in "$wantr|merge_group.base_sha"*) printf 'ok row %-2s REBASE-replayed 3-commit entry -> base = merge_group.base_sha, not the previous commit\n' "$row" ;; + *) printf 'FAIL row %-2s REBASE-replayed entry: wanted %s|merge_group.base_sha..., got %s\n' "$row" "$wantr" "$gotr"; fails=1 ;; esac + row=$((row + 1)) + gotr=$( GITHUB_EVENT_NAME=merge_group GITHUB_EVENT_PATH="$TD/rebase-event.json" ROADMAP_DIFF_NO_EVENT_BASE=1 bash -c 'cd "$2" || exit 2; . "$0" --lib-only; REPO_ROOT="$1"; resolve_base HEAD && printf "%s|%s" "$BASE_REF" "$BASE_HOW"' "$SELF" "$QR.clone" "$QR.clone" 2>/dev/null ) || true + case "$gotr" in "$wantr|"*) printf 'FAIL row %-2s REBASE-replayed entry with the event base ignored: still named the real base — the mutation did not bite\n' "$row"; fails=1 ;; + *) printf 'ok row %-2s REBASE-replayed entry with the event base ignored (mutation): base is the previous commit, not %s\n' "$row" "${wantr:0:9}" ;; esac # #4936 (P6): the same stacked entry with the deepen's first read dead (curl 92). fetch_p6.sh reads the + # The pull_request fallback on a REAL payload shape (#5859): base is {label, ref, repo{...}, sha, user{...}}, + # so a sed that stops at the first `}` returned nothing. Head's sha comes first and must not be taken. + row=$((row + 1)) + rm -rf "${QR:?}.clone"; git clone -q --depth=1 -b main "file://$QR" "$QR.clone" 2>/dev/null; git -C "$QR.clone" fetch -q --depth=1 origin '+queue-base:refs/remotes/origin/main' 2>/dev/null + printf '{"action":"synchronize","pull_request":{"head":{"label":"o:b","ref":"b","sha":"%s","user":{"login":"a"},"repo":{"id":3,"owner":{"login":"o"}}},"base":{"label":"o:main","ref":"main","repo":{"id":3,"owner":{"login":"o"},"license":{"key":"mit"}},"sha":"%s","user":{"login":"o"}}}}\n' "$(git -C "$QR" rev-parse HEAD)" "$wantr" > "$TD/pr-event.json" + gotr=$( GITHUB_EVENT_NAME=pull_request GITHUB_EVENT_PATH="$TD/pr-event.json" bash -c 'cd "$2" || exit 2; . "$0" --lib-only; REPO_ROOT="$1"; resolve_base HEAD && printf "%s|%s" "$BASE_REF" "$BASE_HOW"' "$SELF" "$QR.clone" "$QR.clone" 2>/dev/null ) || true + case "$gotr" in "$wantr|pull_request.base.sha"*) printf 'ok row %-2s pull_request fallback on a real nested payload -> base = base.sha, not head.sha\n' "$row" ;; + *) printf 'FAIL row %-2s pull_request fallback: wanted %s|pull_request.base.sha..., got %s\n' "$row" "$wantr" "$gotr"; fails=1 ;; esac # deepen again and the base still resolves. A planted git logs every fetch read; both must be the # deepen, so a deepen that is one bare read (it dies, and the by-sha fetch after it answers) is RED. # It runs from a cwd outside REPO_ROOT (the shim's own dir), as check_ont_ratchet.sh may (it never cds), diff --git a/scripts/lib/resolve_base.sh b/scripts/lib/resolve_base.sh index d955594dfe..0ffc76eddb 100644 --- a/scripts/lib/resolve_base.sh +++ b/scripts/lib/resolve_base.sh @@ -24,6 +24,25 @@ # stacked entry is refused below, as it was before #4936 when the fetch failed. _RESOLVE_BASE_P6="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/../ci/fetch_p6.sh" || _RESOLVE_BASE_P6="" +# _resolve_event_base: the base sha the CI event itself names (merge_group.base_sha, else +# pull_request.base.sha), or nothing. Under a REBASE merge queue a multi-commit pull request is +# replayed as N commits, so the head's parent is the previous replayed commit, not the base: +# the parent-based rules below would judge only the LAST commit (#5855). Parsed with sed, no jq. +# ROADMAP_DIFF_NO_EVENT_BASE=1 is the case table's mutation. +_resolve_event_base() { + local ev=${GITHUB_EVENT_PATH:-} sha="" + [ "${ROADMAP_DIFF_NO_EVENT_BASE:-0}" != 1 ] && [ -n "$ev" ] && [ -r "$ev" ] || return 0 + case "${GITHUB_EVENT_NAME:-}" in + merge_group) sha=$(sed -n 's/.*"base_sha"[[:space:]]*:[[:space:]]*"\([0-9a-f]\{40\}\)".*/\1/p' "$ev" | head -n 1) ;; + pull_request|pull_request_target) sha=$(awk '{ s = s $0 } END { if (!match(s, /"base"[ \t]*:[ \t]*\{/)) exit; s = substr(s, RSTART + RLENGTH); if (!match(s, /"sha"[ \t]*:[ \t]*"[0-9a-f]+"/)) exit; s = substr(s, RSTART, RLENGTH); sub(/^"sha"[ \t]*:[ \t]*"/, "", s); sub(/"$/, "", s); if (length(s) == 40) print s }' "$ev") ;; + esac + [ -n "$sha" ] || return 0 + git -C "$REPO_ROOT" cat-file -e "$sha^{commit}" 2>/dev/null \ + || git -C "$REPO_ROOT" fetch -q --depth 1 origin "$sha" >/dev/null 2>&1 || true + git -C "$REPO_ROOT" cat-file -e "$sha^{commit}" 2>/dev/null && printf '%s\n' "$sha" + return 0 +} + resolve_base() { local head=$1 mb parents headid main_tip local PROG="${PROG:-resolve_base}" @@ -50,6 +69,10 @@ resolve_base() { printf '%s: %s is the origin/main tip and its first parent is not fetched, so the only base would be %s itself; refused (never the tree against itself). Deepen the checkout: git fetch --deepen=1 origin +refs/heads/main:refs/remotes/origin/main\n' "$PROG" "$head" "$head" >&2 return 1 fi + local evb; evb=$(_resolve_event_base) + if [ -n "$evb" ] && [ "${GITHUB_EVENT_NAME:-}" = merge_group ]; then + BASE_REF="$evb"; BASE_HOW="merge_group.base_sha (event base; a REBASE-replayed head has no parent that names it)"; return 0 + fi mb="" if [ "${ROADMAP_DIFF_FORCE_SHALLOW:-0}" != 1 ]; then mb=$(git -C "$REPO_ROOT" merge-base origin/main "$head" 2>/dev/null || true) @@ -86,6 +109,7 @@ resolve_base() { BASE_REF="$p1"; BASE_HOW="single parent (stacked merge_group entry: the previous entry's squash, fetched by deepening the shallow checkout)"; return 0 fi fi + if [ -n "$evb" ]; then BASE_REF="$evb"; BASE_HOW="pull_request.base.sha (event base; shallow checkout)"; return 0; fi printf '%s: merge-base(origin/main, %s) is unresolvable (shallow checkout) and %s is not a merge commit nor a commit on the origin/main tip,\n' "$PROG" "$head" "$head" >&2 printf ' so no base can be named. A pull_request job checks out refs/pull/N/merge, a merge_group job the queue head; run with an explicit otherwise.\n' >&2 return 1