From 8a1375523f03890b5e59e962a8bcea9f70ccadce Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Fri, 14 Aug 2026 11:04:20 -0400 Subject: [PATCH 1/2] fix(ci): resolve triggered-integration-test base SHA live from refs/heads/main The "Verify PR merge checkout" step in triggered-integration-test.yaml compared the PR merge ref's base parent against `base_sha`, which was read from `gh api pulls/` -> `.base.sha`. That REST field is only refreshed by GitHub on PR synchronize events (e.g. a push to the head branch) and can lag main's actual tip by hours once a PR sits idle while main advances. Meanwhile `refs/pull//merge` is kept continuously current against main, so the two values can disagree even with no race at all, failing the check with "Checked-out merge does not combine the expected base and PR head" (observed on run 31811278059 testing PR #59: cached base_sha pointed at a main commit that was already superseded by 282a337 seven hours earlier). Resolve `base_sha` live via `git ls-remote refs/heads/main` in the same "Resolve source" step that resolves the merge ref, instead of trusting the cached PR resource field, so both values are re-derived consistently with each other. Signed-off-by: Jordi Gil --- .../workflows/triggered-integration-test.yaml | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/triggered-integration-test.yaml b/.github/workflows/triggered-integration-test.yaml index 7f4951d..fb2e460 100644 --- a/.github/workflows/triggered-integration-test.yaml +++ b/.github/workflows/triggered-integration-test.yaml @@ -65,7 +65,7 @@ jobs: head_repo: ${{ steps.pr.outputs.head_repo }} head_branch: ${{ steps.pr.outputs.head_branch }} head_sha: ${{ steps.pr.outputs.head_sha }} - base_sha: ${{ steps.pr.outputs.base_sha }} + base_sha: ${{ steps.source.outputs.base_sha }} steps: - name: Validate PR number if: inputs.pr_number != '' @@ -115,9 +115,16 @@ jobs: echo "head_repo=$(echo "${PR_JSON}" | jq -r '.head.repo.full_name')" echo "head_branch=$(echo "${PR_JSON}" | jq -r '.head.ref')" echo "head_sha=$(echo "${PR_JSON}" | jq -r '.head.sha')" - echo "base_sha=$(echo "${PR_JSON}" | jq -r '.base.sha')" } >> "${GITHUB_OUTPUT}" + # NOTE: intentionally not capturing `.base.sha` here as the + # verified base — GitHub only refreshes this REST field on PR + # synchronize events (e.g. a push to the head branch), so it can + # lag `main`'s actual tip by hours if the PR sits idle while main + # moves forward. `refs/pull//merge`, resolved in the next step, + # IS kept continuously current against `main`, so the base used + # for verification is instead re-resolved live from + # refs/heads/main in the "Resolve source" step below. echo "${PR_JSON}" | jq '{ number: .number, title: .title, @@ -129,7 +136,7 @@ jobs: head_branch: .head.ref, head_sha: .head.sha, base_branch: .base.ref, - base_sha: .base.sha + base_sha_at_pr_query: .base.sha }' > source-metadata.json echo "::group::PR metadata" @@ -152,12 +159,27 @@ jobs: echo "::error::The PR may have merge conflicts or the merge ref may not exist" exit 1 fi + + # Resolve the base live from refs/heads/main, right alongside + # the merge ref above, instead of trusting the PR REST + # resource's `.base.sha` (see note in "Query PR metadata"). + # refs/pull//merge is kept in sync with main automatically, + # so re-deriving the expected base the same way keeps both + # values consistent with each other. + BASE_SHA=$(git ls-remote "${REPO_URL}" refs/heads/main | awk '{print $1}') + if [[ -z "${BASE_SHA}" ]]; then + echo "::error::Cannot resolve refs/heads/main for base verification" + exit 1 + fi + { echo "source_kind=pr" echo "source_label=PR #${PR_NUM}" echo "tested_sha=${MERGE_SHA}" + echo "base_sha=${BASE_SHA}" } >> "${GITHUB_OUTPUT}" echo "Resolved refs/pull/${PR_NUM}/merge -> ${MERGE_SHA}" + echo "Resolved refs/heads/main -> ${BASE_SHA} (expected base)" else MAIN_SHA=$(git ls-remote "${REPO_URL}" refs/heads/main | awk '{print $1}') if [[ -z "${MAIN_SHA}" ]]; then From 9fd7f8392aa997bdfa373c26844573b35927d350 Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Fri, 14 Aug 2026 11:41:42 -0400 Subject: [PATCH 2/2] fix(ci): resolve merge and base SHAs from one ls-remote call Addresses grid#62 review feedback (praxis-bot): the merge-ref and refs/heads/main resolutions were two sequential `git ls-remote` calls, leaving a window for main to advance between them -- the same class of staleness bug this PR fixes, just with a much narrower race window. Combine both into a single `git ls-remote` invocation so both SHAs are resolved from the same server-side view, making them atomically consistent with each other instead of merely "re-derived the same way." Signed-off-by: Jordi Gil --- .../workflows/triggered-integration-test.yaml | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/triggered-integration-test.yaml b/.github/workflows/triggered-integration-test.yaml index fb2e460..f1635af 100644 --- a/.github/workflows/triggered-integration-test.yaml +++ b/.github/workflows/triggered-integration-test.yaml @@ -152,21 +152,22 @@ jobs: REPO_URL="https://github.com/${{ github.repository }}.git" if [[ -n "${PR_NUM}" ]]; then - MERGE_SHA=$(git ls-remote "${REPO_URL}" \ - "refs/pull/${PR_NUM}/merge" | awk '{print $1}') + # Resolve the merge ref and refs/heads/main in a single + # `git ls-remote` call, instead of trusting the PR REST + # resource's `.base.sha` (see note in "Query PR metadata") and + # instead of two sequential round-trips, which would leave a + # window for main to advance between them and make BASE_SHA + # point past the merge ref's actual first parent. One call + # resolves both refs from the same server-side view, making + # them atomically consistent with each other. + LS_OUTPUT=$(git ls-remote "${REPO_URL}" "refs/pull/${PR_NUM}/merge" refs/heads/main) + MERGE_SHA=$(echo "${LS_OUTPUT}" | awk '/refs\/pull\//{print $1}') + BASE_SHA=$(echo "${LS_OUTPUT}" | awk '/refs\/heads\/main/{print $1}') if [[ -z "${MERGE_SHA}" ]]; then echo "::error::Cannot resolve refs/pull/${PR_NUM}/merge" echo "::error::The PR may have merge conflicts or the merge ref may not exist" exit 1 fi - - # Resolve the base live from refs/heads/main, right alongside - # the merge ref above, instead of trusting the PR REST - # resource's `.base.sha` (see note in "Query PR metadata"). - # refs/pull//merge is kept in sync with main automatically, - # so re-deriving the expected base the same way keeps both - # values consistent with each other. - BASE_SHA=$(git ls-remote "${REPO_URL}" refs/heads/main | awk '{print $1}') if [[ -z "${BASE_SHA}" ]]; then echo "::error::Cannot resolve refs/heads/main for base verification" exit 1