From 99e016e314e205116b273e30c904d4c064cecffa Mon Sep 17 00:00:00 2001 From: Misha Kav Date: Mon, 31 Aug 2026 17:29:10 +0300 Subject: [PATCH 1/3] fix: fetch fork PR heads via refs/pull/N/head [LINBEE-28634] Co-Authored-By: Claude Opus 5 (1M context) --- action.yml | 7 ++++++- scripts/resolve-payload-fields.js | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/action.yml b/action.yml index a15150f6..e49c2683 100644 --- a/action.yml +++ b/action.yml @@ -104,7 +104,12 @@ runs: git config checkout.defaultRemote origin git fetch --shallow-since="6 months ago" origin $'${{ steps.safe-strings.outputs.base_ref }}' git remote add upstream $'${{ steps.safe-strings.outputs.url }}' - git fetch --shallow-since="6 months ago" upstream $'${{ steps.safe-strings.outputs.head_ref }}' || echo "::warning::Failed to fetch head branch. The branch may have been deleted." + # GitHub publishes every PR head under the base repo's own refs/pull/*, so a + # fork head needs no access to the fork. Falls back to the head remote when + # the PR number is absent (non-PR events). + git fetch --shallow-since="6 months ago" origin $'refs/pull/${{ steps.payload-fields.outputs.pull_request_number }}/head:refs/remotes/upstream/${{ steps.safe-strings.outputs.head_ref }}' \ + || git fetch --shallow-since="6 months ago" upstream $'${{ steps.safe-strings.outputs.head_ref }}' \ + || echo "::warning::Failed to fetch head branch. The branch may have been deleted." git checkout -b $'upstream/${{ steps.safe-strings.outputs.head_ref}}' $'upstream/${{ steps.safe-strings.outputs.head_ref}}' || true git checkout $'${{ steps.safe-strings.outputs.base_ref }}' || true git checkout $'${{ steps.safe-strings.outputs.head_ref }}' || true diff --git a/scripts/resolve-payload-fields.js b/scripts/resolve-payload-fields.js index 717c7ff5..2d11c25d 100644 --- a/scripts/resolve-payload-fields.js +++ b/scripts/resolve-payload-fields.js @@ -158,6 +158,9 @@ function toStepOutputs(payload) { return { github_token: payload.githubToken || '', url: payload.headHttpUrl || payload.repoUrl || '', + // Coerced so a missing or non-numeric value yields '', which fails the + // refs/pull fetch fast and falls back to fetching the head remote directly. + pull_request_number: String(Number(payload.pullRequestNumber) || ''), has_cm_repo: String(hasCmRepo), cm_repository: hasCmRepo ? `${payload.owner}/${payload.cmRepo}` : '', cm_repo_ref: payload.cmRepoRef || '', From 94536e85a2a216aad711a8a8ef52903e45ea6e1d Mon Sep 17 00:00:00 2001 From: Misha Kav Date: Tue, 1 Sep 2026 10:00:18 +0300 Subject: [PATCH 2/3] refactor: drop unreachable fetch fallback, cover pull_request_number in tests Co-Authored-By: Claude Opus 5 (1M context) --- __tests__/resolve-payload-fields.test.ts | 3 +++ action.yml | 8 ++------ scripts/resolve-payload-fields.js | 3 +-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/__tests__/resolve-payload-fields.test.ts b/__tests__/resolve-payload-fields.test.ts index 0db6a3c6..5aa52ab7 100644 --- a/__tests__/resolve-payload-fields.test.ts +++ b/__tests__/resolve-payload-fields.test.ts @@ -14,6 +14,7 @@ const payload = { githubToken: 'ghs_token', headHttpUrl: 'https://github.com/acme/repo.git', repoUrl: 'https://github.com/acme/other.git', + pullRequestNumber: 269, owner: 'acme', hasCmRepo: true, cmRepo: 'cm-repo', @@ -50,6 +51,7 @@ describe('toStepOutputs', () => { expect(toStepOutputs(payload)).toEqual({ github_token: 'ghs_token', url: 'https://github.com/acme/repo.git', + pull_request_number: '269', has_cm_repo: 'true', cm_repository: 'acme/cm-repo', cm_repo_ref: 'main', @@ -64,6 +66,7 @@ describe('toStepOutputs', () => { ).toEqual({ github_token: '', url: 'https://github.com/acme/other.git', + pull_request_number: '', has_cm_repo: 'false', cm_repository: '', cm_repo_ref: '', diff --git a/action.yml b/action.yml index e49c2683..341089bd 100644 --- a/action.yml +++ b/action.yml @@ -104,12 +104,8 @@ runs: git config checkout.defaultRemote origin git fetch --shallow-since="6 months ago" origin $'${{ steps.safe-strings.outputs.base_ref }}' git remote add upstream $'${{ steps.safe-strings.outputs.url }}' - # GitHub publishes every PR head under the base repo's own refs/pull/*, so a - # fork head needs no access to the fork. Falls back to the head remote when - # the PR number is absent (non-PR events). - git fetch --shallow-since="6 months ago" origin $'refs/pull/${{ steps.payload-fields.outputs.pull_request_number }}/head:refs/remotes/upstream/${{ steps.safe-strings.outputs.head_ref }}' \ - || git fetch --shallow-since="6 months ago" upstream $'${{ steps.safe-strings.outputs.head_ref }}' \ - || echo "::warning::Failed to fetch head branch. The branch may have been deleted." + # refs/pull/N/head is mirrored on the base repo, so a fork PR head needs no fork access. + git fetch --shallow-since="6 months ago" origin $'refs/pull/${{ steps.payload-fields.outputs.pull_request_number }}/head:refs/remotes/upstream/${{ steps.safe-strings.outputs.head_ref }}' || echo "::warning::Failed to fetch head branch. The branch may have been deleted." git checkout -b $'upstream/${{ steps.safe-strings.outputs.head_ref}}' $'upstream/${{ steps.safe-strings.outputs.head_ref}}' || true git checkout $'${{ steps.safe-strings.outputs.base_ref }}' || true git checkout $'${{ steps.safe-strings.outputs.head_ref }}' || true diff --git a/scripts/resolve-payload-fields.js b/scripts/resolve-payload-fields.js index 2d11c25d..4f349358 100644 --- a/scripts/resolve-payload-fields.js +++ b/scripts/resolve-payload-fields.js @@ -158,8 +158,7 @@ function toStepOutputs(payload) { return { github_token: payload.githubToken || '', url: payload.headHttpUrl || payload.repoUrl || '', - // Coerced so a missing or non-numeric value yields '', which fails the - // refs/pull fetch fast and falls back to fetching the head remote directly. + // Digits or '': spliced into the fetch refspec without the safe-strings escaping step. pull_request_number: String(Number(payload.pullRequestNumber) || ''), has_cm_repo: String(hasCmRepo), cm_repository: hasCmRepo ? `${payload.owner}/${payload.cmRepo}` : '', From 82fd67135a36513698d8f7420dd80ba6e5d4c803 Mon Sep 17 00:00:00 2001 From: Misha Kav Date: Tue, 1 Sep 2026 11:59:15 +0300 Subject: [PATCH 3/3] fix: constrain pull_request_number to a positive integer Co-Authored-By: Claude Opus 5 (1M context) --- __tests__/resolve-payload-fields.test.ts | 9 +++++++++ scripts/resolve-payload-fields.js | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/__tests__/resolve-payload-fields.test.ts b/__tests__/resolve-payload-fields.test.ts index 5aa52ab7..350cc200 100644 --- a/__tests__/resolve-payload-fields.test.ts +++ b/__tests__/resolve-payload-fields.test.ts @@ -60,6 +60,15 @@ describe('toStepOutputs', () => { }) }) + it.each([1.2, -1, 0, Infinity, 'abc'])( + 'blanks a pull request number that is not a positive integer: %p', + pullRequestNumber => { + expect( + toStepOutputs({ ...payload, pullRequestNumber }).pull_request_number + ).toBe('') + } + ) + it('falls back to repoUrl and blanks the cm repo when absent', () => { expect( toStepOutputs({ repoUrl: 'https://github.com/acme/other.git' }) diff --git a/scripts/resolve-payload-fields.js b/scripts/resolve-payload-fields.js index 4f349358..35edde91 100644 --- a/scripts/resolve-payload-fields.js +++ b/scripts/resolve-payload-fields.js @@ -155,11 +155,13 @@ function normalizeForEngine(raw) { */ function toStepOutputs(payload) { const hasCmRepo = payload.hasCmRepo === true + const prNumber = Number(payload.pullRequestNumber) + // A positive integer or '': spliced into the fetch refspec without the safe-strings escaping step. + const isPrNumber = Number.isInteger(prNumber) && prNumber > 0 return { github_token: payload.githubToken || '', url: payload.headHttpUrl || payload.repoUrl || '', - // Digits or '': spliced into the fetch refspec without the safe-strings escaping step. - pull_request_number: String(Number(payload.pullRequestNumber) || ''), + pull_request_number: isPrNumber ? String(prNumber) : '', has_cm_repo: String(hasCmRepo), cm_repository: hasCmRepo ? `${payload.owner}/${payload.cmRepo}` : '', cm_repo_ref: payload.cmRepoRef || '',