From 4d7e7cdfc00e56ebcc57be5324b833aa094a136d Mon Sep 17 00:00:00 2001 From: Chris Wolfgang Date: Sat, 19 Sep 2026 12:00:25 -0400 Subject: [PATCH] ci(guard): find the merge base on a shallow checkout and fail closed when it cannot repo-template#584: the detect job's checkout is depth 1, so the guard's three-dot diff had no merge base, failed silently and reported "no protected configuration files changed" for every PR. Deepen the PR head until a merge base exists, diff against it explicitly, and error out if none can be found. Co-Authored-By: Claude Opus 5 --- .github/workflows/pr.yaml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index f81af8bc..3c73732b 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -174,6 +174,11 @@ jobs: # in their own PRs; it does not apply to a trusted GitHub-controlled bot whose # only action is package-version updates. if: github.event.pull_request.user.login != 'dependabot[bot]' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + # persist-credentials:false on the checkout, so the deepening fetch below needs the + # job token for a private repository. Used on this one command only; never stored. + TRUSTED_MAIN_URL: https://x-access-token:${{ github.token }}@github.com/${{ github.repository }} run: | echo "Checking for changes to protected configuration files in this PR..." @@ -189,7 +194,18 @@ jobs: # a PR that *deletes* a workflow or .globalconfig is as security-relevant as an # edit. -i: Windows (and ReSharper on it) resolve names case-insensitively, so # foo.dotsettings is as live as foo.DotSettings. - mapfile -t all_changed < <(git diff --name-only --diff-filter=AMRCD main-branch...HEAD 2>/dev/null) + # The checkout is shallow (depth 1); the PR's own history is needed to find the + # merge base. Deepen until one exists, then diff against it. Fail closed: a guard + # that cannot compute the diff must not report "nothing changed". + for _ in 1 2 3 4 5 6; do + git merge-base main-branch HEAD >/dev/null 2>&1 && break + git fetch --quiet --deepen=50 "$TRUSTED_MAIN_URL" "refs/pull/$PR_NUMBER/head" 2>/dev/null || true + done + if ! base=$(git merge-base main-branch HEAD 2>/dev/null); then + echo "::error::Cannot find the merge base between main and the PR head - refusing to classify the change." + exit 1 + fi + mapfile -t all_changed < <(git diff --name-only --diff-filter=AMRCD "$base" HEAD) protected=(); other=() for file in "${all_changed[@]}"; do case "$file" in