diff --git a/.changeset/sdk-80-labeler-workflow.md b/.changeset/sdk-80-labeler-workflow.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/sdk-80-labeler-workflow.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.github/workflows/labeler-apply.yml b/.github/workflows/labeler-apply.yml new file mode 100644 index 00000000000..9ace7dbc33d --- /dev/null +++ b/.github/workflows/labeler-apply.yml @@ -0,0 +1,75 @@ +name: Labeler (apply) + +# Privileged half of the labeler. Triggered by completion of the "Labeler" workflow, +# so it always runs the base-branch copy of this file (a PR cannot modify it) with +# write access. It reads the PR number from the trigger run's artifact, validates it, +# checks out only the trusted base .github/labeler.yml, and applies labels via +# actions/labeler driven by pr-number. Replaces the previous pull_request_target +# trigger while preserving fork-PR labeling (SDK-80). + +on: + workflow_run: + workflows: [Labeler] + types: + - completed + +permissions: {} + +jobs: + apply: + name: Apply labels + if: >- + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + timeout-minutes: ${{ vars.TIMEOUT_MINUTES_SHORT && fromJSON(vars.TIMEOUT_MINUTES_SHORT) || 3 }} + runs-on: ${{ vars.RUNNER_NORMAL || 'ubuntu-latest' }} + permissions: + actions: read # download the artifact from the triggering run + contents: read # checkout the trusted base labeler config + pull-requests: write # apply labels + steps: + - name: Download PR number + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: pr-number + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Resolve and verify PR number + id: pr + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const fs = require('fs'); + const raw = fs.readFileSync('number', 'utf8').trim(); + if (!/^[0-9]+$/.test(raw)) { + return core.setFailed(`Invalid PR number from artifact: '${raw}'`); + } + // The artifact comes from the untrusted pull_request leg, so its number + // could point at any PR. Bind it to the run that actually triggered this + // workflow_run before handing it to the privileged labeler: the claimed + // PR's head commit and head repository must match the trigger run. + const run = context.payload.workflow_run; + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: Number(raw), + }); + if (pr.head.sha !== run.head_sha) { + return core.setFailed(`PR #${raw} head ${pr.head.sha} does not match triggering run head ${run.head_sha}`); + } + const prHeadRepo = pr.head.repo ? pr.head.repo.full_name : null; + const runHeadRepo = run.head_repository ? run.head_repository.full_name : null; + if (prHeadRepo !== runHeadRepo) { + return core.setFailed(`PR #${raw} head repo ${prHeadRepo} does not match triggering run head repo ${runHeadRepo}`); + } + core.setOutput('number', raw); + - name: Check out trusted labeler config + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + sparse-checkout: .github/labeler.yml + sparse-checkout-cone-mode: false + persist-credentials: false + - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6 + with: + pr-number: ${{ steps.pr.outputs.number }} + configuration-path: .github/labeler.yml diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index d27fdec4e32..1c68d4b93f7 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,14 +1,29 @@ name: Labeler +# Untrusted trigger half of the labeler. Runs in the PR (incl. fork) context with a +# read-only token and no secrets. It only records the PR number to an artifact; the +# privileged labeling happens in labeler-apply.yml on workflow_run. This avoids +# pull_request_target (see SDK-80 / Monorepo Supply-Chain Hardening). + on: - - pull_request_target + - pull_request + +permissions: {} jobs: - triage: + collect: + name: Collect PR metadata timeout-minutes: ${{ vars.TIMEOUT_MINUTES_SHORT && fromJSON(vars.TIMEOUT_MINUTES_SHORT) || 3 }} - permissions: - contents: read - pull-requests: write runs-on: ${{ vars.RUNNER_NORMAL || 'ubuntu-latest' }} steps: - - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6 + - name: Save PR number + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + mkdir -p ./pr + echo "$PR_NUMBER" > ./pr/number + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: pr-number + path: ./pr/ + retention-days: 1