diff --git a/ci/sections.yml b/ci/sections.yml index 249aedae8c..188f01f568 100644 --- a/ci/sections.yml +++ b/ci/sections.yml @@ -1147,18 +1147,34 @@ jobs: # never as "no growth", so the mistake would have been loud rather than # silent. It is still a mistake. One fetch, before all of them. # - # fetch-depth is 1 here, so the merge-base is usually unavailable and the - # ratchets fall back to the origin/main TIP. That is the STRICTER - # comparand, and it means the CI path is the tip path: a local run that - # passes on the merge-base can still red here, and CI is authoritative. + # #4861 + #4983: origin/main is pinned to the commit this tree was BUILT + # on, not fetched at its tip. fetch-depth is 1 here, so the merge-base is + # usually unavailable and the ratchets read origin/main as it stands. + # Fetched at the tip, a run that started after main shrank a baseline or + # fixed a function judged a merge commit built before that change against + # it: #4813 went red (pipe_grep_q_baseline.txt 62 vs 60, a file it never + # touched), and #4970's re-run went red on m17 complexity (checkout at + # 60a59031, compared against acd599e52), while a re-run rebuilds the SAME + # merge commit. Pinned per event (scripts/ci/pin_build_base.sh): + # pull_request the merge commit's first parent, checked against the PR head + # merge_group merge_group.base_sha, as the refinement-gate pin does + # otherwise the tip, deepened by one on push (resolve_base.sh judges a + # push against HEAD's first parent: HEAD vs HEAD would pass + # vacuously, the G-10 quorum's finding) + # The shared ratchet library takes the same first parent on its own + # (scripts/lib_baseline_ratchet.sh FIRSTPARENT), so a lib ratchet and a + # script that reads origin/main directly cannot disagree. A PR that adds + # back an entry main has since deleted is still refused by its + # merge_group run, which builds on main as it merges. The pin refuses, + # and every guard below is skipped red, when the event names no build base. - id: guard-setup - name: Fetch origin/main as the comparand for every ratchet in this job + name: Pin origin/main to this run's build base, the comparand for every ratchet in this job (#4861) + env: + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + MG_BASE_SHA: ${{ github.event.merge_group.base_sha }} run: | - git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main - # push shape: HEAD IS the origin/main tip, so every differential ratchet judges it against - # its FIRST PARENT (scripts/lib/resolve_base.sh: HEAD vs HEAD would pass vacuously, the - # G-10 quorum's finding), and a depth-1 checkout has not fetched that parent. Deepen by one. - if [ "${GITHUB_EVENT_NAME:-}" = push ]; then git fetch --no-tags --deepen=1 origin +refs/heads/main:refs/remotes/origin/main; fi + bash scripts/ci/pin_build_base.sh --self-test + bash scripts/ci/pin_build_base.sh # #4415 (operator A4, 2026-09-25): ONE step runs every guard step of guard-tree-steps # below, keeps going past a red one, and reports every failure. GitHub would # stop this job at its first red step, and the others would never be seen. @@ -1685,17 +1701,16 @@ jobs: # never as "no growth", so the mistake would have been loud rather than # silent. It is still a mistake. One fetch, before all of them. # - # fetch-depth is 1 here, so the merge-base is usually unavailable and the - # ratchets fall back to the origin/main TIP. That is the STRICTER - # comparand, and it means the CI path is the tip path: a local run that - # passes on the merge-base can still red here, and CI is authoritative. - - name: Fetch origin/main as the comparand for every ratchet in this job - run: | - git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main - # push shape: HEAD IS the origin/main tip, so every differential ratchet judges it against - # its FIRST PARENT (scripts/lib/resolve_base.sh: HEAD vs HEAD would pass vacuously, the - # G-10 quorum's finding), and a depth-1 checkout has not fetched that parent. Deepen by one. - if [ "${GITHUB_EVENT_NAME:-}" = push ]; then git fetch --no-tags --deepen=1 origin +refs/heads/main:refs/remotes/origin/main; fi + # #4861 + #4983: origin/main is pinned to the commit this tree was BUILT + # on, not fetched at its tip -- the same pin, for the same reason, as + # guard-tree's guard-setup (which also runs the pin's self-test; this job + # only pins). m17's complexity ratchet is the #4983 case: a re-run of an + # unchanged PR judged against main's moved tip went red. + - name: Pin origin/main to this run's build base, the comparand for every ratchet in this job (#4861) + env: + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + MG_BASE_SHA: ${{ github.event.merge_group.base_sha }} + run: bash scripts/ci/pin_build_base.sh # DOCKER CREATES A MISSING BIND SOURCE AS root:root, AND `--user` ALONE # THEN EACCESs ON IT. The per-RUN target path is fresh every run, so a # container that mounts it and drops to the runner's uid cannot write diff --git a/scripts/check_baseline_ratchets.sh b/scripts/check_baseline_ratchets.sh index 2952390bb2..3a8ea7fc31 100644 --- a/scripts/check_baseline_ratchets.sh +++ b/scripts/check_baseline_ratchets.sh @@ -372,6 +372,107 @@ if [ "${1:-}" = "--self-test" ] || [ "${1:-}" = "--selftest" ]; then bad=1 fi + # FIRSTPARENT (#4983): a pull_request re-run. CI checks out GitHub's + # merge commit at depth 1, so there is no merge-base, and the re-run + # fetches origin/main fresh. Here main moved on and SHRANK the + # baseline (b deleted). Against the tip, the PR "appends" b, an + # entry it never touched; against the first parent, it changed + # nothing. The shallow clone is its own repository under $TD. + SC="$TD/shallow" + if git -C "$SR" checkout -q -b fp-pr "$SR_BASE" >/dev/null 2>&1 \ + && printf 'pr edit\n' > "$SR/scripts/unrelated.txt" \ + && git -C "$SR" -c commit.gpgsign=false commit -qam 'pr: unrelated edit' >/dev/null 2>&1 \ + && SR_X=$(git -C "$SR" rev-parse HEAD) \ + && git -C "$SR" checkout -q --detach "$SR_BASE" >/dev/null 2>&1 \ + && git -C "$SR" -c commit.gpgsign=false merge -q --no-ff --no-edit "$SR_X" >/dev/null 2>&1 \ + && SR_M=$(git -C "$SR" rev-parse HEAD) \ + && git -C "$SR" update-ref refs/pull/1/merge "$SR_M" \ + && git -C "$SR" checkout -q --detach "$SR_NOBASE" >/dev/null 2>&1 \ + && git -C "$SR" -c commit.gpgsign=false merge -q --no-ff --no-edit "$SR_BASE" >/dev/null 2>&1 \ + && SR_M2=$(git -C "$SR" rev-parse HEAD) \ + && git -C "$SR" update-ref refs/pull/2/merge "$SR_M2" \ + && git -C "$SR" checkout -q --detach "$SR_BASE" >/dev/null 2>&1 \ + && printf '# header\na\n' > "$SR/$P" \ + && git -C "$SR" -c commit.gpgsign=false commit -qam 'main: shrink the baseline' >/dev/null 2>&1 \ + && git -C "$SR" update-ref refs/heads/fp-main "$(git -C "$SR" rev-parse HEAD)" \ + && git -C "$SR" checkout -q --detach "$SR_BASE" >/dev/null 2>&1 \ + && git -C "$SR" config uploadpack.allowAnySHA1InWant true \ + && git init -q "$SC" >/dev/null 2>&1 \ + && [ "$(git -C "$SC" rev-parse --absolute-git-dir 2>/dev/null)" = "$SC/.git" ] \ + && git -C "$SC" fetch -q --no-tags --depth=1 "file://$SR" '+refs/pull/1/merge:refs/remotes/pull/1/merge' >/dev/null 2>&1 \ + && git -C "$SC" checkout -q --detach refs/remotes/pull/1/merge >/dev/null 2>&1 \ + && git -C "$SC" fetch -q --no-tags --depth=1 "file://$SR" '+refs/heads/fp-main:refs/remotes/origin/main' >/dev/null 2>&1 \ + && [ -z "$(git -C "$SC" merge-base HEAD origin/main 2>/dev/null)" ] \ + && ! git -C "$SC" cat-file -e "${SR_BASE}^{commit}" 2>/dev/null; then + + fp_row() { # fp_row