fix(ci): run PR LoC scripts from the default branch, not PR head (#15016)

The PR test LoC job fetched .github/scripts/pr-test-loc-*.mjs from
pull/<n>/head and ran them with node while holding a GITHUB_TOKEN scoped
pull-requests: write, so PR-authored code executed under a write token.

Pin the fetch to the repository default branch. base.sha is not enough:
for stacked PRs it is an unreviewed feature-branch commit any collaborator
can push to, while main is gated by branch protection.

Also pass event data via env instead of shell interpolation, and add
set -euo pipefail so a failed download cannot leave a truncated script.
This commit is contained in:
Neil
2026-08-16 22:18:50 -07:00
committed by GitHub
parent 8ca4ed945e
commit 08bf209e40
2 changed files with 42 additions and 15 deletions
+8 -2
View File
@@ -23,14 +23,20 @@ jobs:
timeout-minutes: 2
steps:
# Why no checkout: the Files API already has per-file additions/deletions.
# Why the default branch and never pull/<n>/head: this job holds a write-scoped
# GITHUB_TOKEN, so it may only execute reviewed code. A PR that edits these
# scripts takes effect once merged.
- name: Count test vs non-test LoC
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
TRUSTED_REF: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
for script in pr-test-loc-table.mjs pr-test-loc-summary.mjs; do
gh api "repos/${GITHUB_REPOSITORY}/contents/.github/scripts/${script}?ref=pull/${{ github.event.pull_request.number }}/head" \
gh api "repos/${GITHUB_REPOSITORY}/contents/.github/scripts/${script}?ref=${TRUSTED_REF}" \
--jq .content | base64 --decode > "$RUNNER_TEMP/${script}"
done
node "$RUNNER_TEMP/pr-test-loc-summary.mjs" --update-pr "${{ github.event.pull_request.number }}"
node "$RUNNER_TEMP/pr-test-loc-summary.mjs" --update-pr "$PR_NUMBER"
+34 -13
View File
@@ -15,6 +15,11 @@ import {
const projectDir = resolve(import.meta.dirname, '../..')
const locScript = join(projectDir, '.github/scripts/pr-test-loc-summary.mjs')
const locWorkflow = parse(
readFileSync(join(projectDir, '.github/workflows/pr-test-loc.yml'), 'utf8')
)
const locJob = locWorkflow.jobs.loc
const locStep = locJob.steps[0]
const tempDirs = []
function runLoc(args, { env } = {}) {
@@ -178,20 +183,36 @@ describe('PR test LoC summary', () => {
})
it('is a no-checkout GitHub-hosted PR workflow', () => {
const workflow = parse(
readFileSync(join(projectDir, '.github/workflows/pr-test-loc.yml'), 'utf8')
)
const locJob = workflow.jobs.loc
const serialized = JSON.stringify(workflow)
expect(locJob['runs-on']).toBe('ubuntu-latest')
expect(locJob.steps).toHaveLength(1)
expect(locJob.steps[0].run).toContain('gh api')
expect(locJob.steps[0].run).toContain('pr-test-loc-table.mjs')
expect(locJob.steps[0].run).toContain('pr-test-loc-summary.mjs')
expect(locJob.steps[0].run).toContain('--update-pr')
expect(workflow.permissions['pull-requests']).toBe('write')
expect(serialized).not.toContain('actions/checkout')
expect(serialized).not.toContain('self-hosted')
expect(locStep.run).toContain('gh api')
expect(locStep.run).toContain('pr-test-loc-table.mjs')
expect(locStep.run).toContain('pr-test-loc-summary.mjs')
expect(locStep.run).toContain('--update-pr')
expect(locWorkflow.permissions['pull-requests']).toBe('write')
expect(JSON.stringify(locWorkflow)).not.toContain('actions/checkout')
expect(JSON.stringify(locWorkflow)).not.toContain('self-hosted')
})
// The write-scoped GITHUB_TOKEN makes any PR-authored code a privilege escalation.
it('executes only default-branch script code, never pull-request head code', () => {
const serialized = JSON.stringify(locWorkflow)
expect(locStep.env.TRUSTED_REF).toBe('${{ github.event.repository.default_branch }}')
expect(locStep.run).toContain('?ref=${TRUSTED_REF}')
expect(serialized).not.toContain('pull_request_target')
expect(serialized).not.toContain('pull/')
expect(serialized).not.toContain('pull_request.head')
expect(serialized).not.toContain('/merge')
})
it('passes event data through env instead of interpolating it into the shell', () => {
expect(locStep.env.PR_NUMBER).toBe('${{ github.event.pull_request.number }}')
expect(locStep.run).toContain('--update-pr "$PR_NUMBER"')
expect(locStep.run).not.toContain('${{')
})
it('fails the step when a script download fails instead of running a truncated file', () => {
expect(locStep.run).toContain('set -euo pipefail')
})
})