From 88f964ead486052df6963f026a3823ac0a009566 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 9 Sep 2026 12:54:57 -0500 Subject: [PATCH] Playground preview: simplify artifact handoff to the publisher Resolve the pull request from the workflow_run payload instead of a metadata artifact, so the build workflow only produces the plugin zip. Have the publisher download that zip itself, check that it is a plain, valid zip file, and pass it to the expose action through the current run rather than the build run. Pin the publish workflow's actions to commits, stop persisting checkout credentials in the build, and drop the stale trigger name. Co-Authored-By: Claude Fable 5.1 --- .../pr-playground-preview-publish.yml | 102 +++++++++--------- .github/workflows/pr-playground-preview.yml | 20 +--- 2 files changed, 55 insertions(+), 67 deletions(-) diff --git a/.github/workflows/pr-playground-preview-publish.yml b/.github/workflows/pr-playground-preview-publish.yml index 98bfe78aa..3c70697d0 100644 --- a/.github/workflows/pr-playground-preview-publish.yml +++ b/.github/workflows/pr-playground-preview-publish.yml @@ -2,12 +2,12 @@ name: PR Playground Preview Publish # Publishes the read-only build artifact from "PR Playground Preview Build" and # writes the Playground button to the PR. This workflow must not check out or -# execute pull request code. +# execute pull request code, and must treat everything the build run produced +# as untrusted. on: workflow_run: workflows: - - PR Playground Preview - PR Playground Preview Build types: - completed @@ -25,73 +25,71 @@ jobs: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' steps: - - name: Download Playground preview metadata - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RUN_ID: ${{ github.event.workflow_run.id }} - run: | - gh run download "$RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --name plugin-check-preview-metadata \ - --dir preview-metadata - - - name: Resolve and verify Playground preview metadata + - name: Resolve pull request from the triggering run id: metadata env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} - # The build runs untrusted PR code, so its artifact is untrusted; the - # triggering run's head SHA is the only reliable value. - TRUSTED_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + # The build runs untrusted PR code, so nothing it uploads can identify + # the PR; only the triggering run's own metadata can. + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} run: | - artifact_pr_number="$(cat preview-metadata/pr-number)" - artifact_head_sha="$(cat preview-metadata/head-sha)" + # workflow_run.pull_requests is empty for fork PRs; match repo, branch and SHA so no other PR at this commit can claim the run. + pr_numbers="$( + gh api "repos/${GH_REPO}/pulls?state=open&per_page=100" --paginate \ + --jq '.[] | select(.head.sha == env.HEAD_SHA and .head.repo.full_name == env.HEAD_REPO and .head.ref == env.HEAD_BRANCH) | .number' + )" + # First match; piping into head instead would SIGPIPE gh under pipefail. + pr_number="${pr_numbers%%$'\n'*}" - if ! [[ "$artifact_head_sha" =~ ^[0-9a-f]{40}$ ]]; then - echo "Invalid head SHA in preview metadata: $artifact_head_sha" >&2 + if ! [[ "$pr_number" =~ ^[0-9]+$ ]]; then + echo "Could not resolve an open pull request for ${HEAD_REPO}@${HEAD_BRANCH} (${HEAD_SHA})." >&2 exit 1 fi - if [[ "$artifact_head_sha" != "$TRUSTED_HEAD_SHA" ]]; then - echo "Refusing to publish: artifact head SHA ($artifact_head_sha) does not match the triggering run's head SHA ($TRUSTED_HEAD_SHA)." >&2 - exit 1 - fi + echo "pr-number=$pr_number" >> "$GITHUB_OUTPUT" - # Resolve the PR number from the trusted head SHA rather than the - # artifact-supplied value. The commits/{sha}/pulls association index - # omits fork PR heads, so list open PRs and match on the exact head - # SHA instead; this also prevents resolving a different PR that - # merely contains the commit. - resolved_pr_number="$( - gh api "repos/${GH_REPO}/pulls?state=open&per_page=100" --paginate \ - --jq '.[] | select(.head.sha == env.TRUSTED_HEAD_SHA) | .number' \ - | head -n 1 - )" + - name: Download and verify plugin zip artifact + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RUN_ID: ${{ github.event.workflow_run.id }} + run: | + gh run download "$RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --name plugin-check-zip \ + --dir plugin-zip - if ! [[ "$resolved_pr_number" =~ ^[0-9]+$ ]]; then - echo "Could not resolve an open pull request for head SHA $TRUSTED_HEAD_SHA." >&2 + # Untrusted artifact: publish only a regular file that is a valid zip. + zip_path=plugin-zip/plugin-check.zip + if [[ -L "$zip_path" || ! -f "$zip_path" ]]; then + echo "Refusing to publish: '$zip_path' is not a regular file." >&2 exit 1 fi + unzip -tq "$zip_path" - if [[ "$artifact_pr_number" != "$resolved_pr_number" ]]; then - echo "Notice: artifact PR number ($artifact_pr_number) does not match the PR resolved from the head SHA ($resolved_pr_number); using the resolved value." >&2 - fi - - echo "pr-number=$resolved_pr_number" >> "$GITHUB_OUTPUT" - echo "head-sha=$TRUSTED_HEAD_SHA" >> "$GITHUB_OUTPUT" + # The expose action does not check artifacts from other runs, so hand it this run's verified copy. + - name: Upload verified plugin zip for publishing + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: plugin-check-zip + path: plugin-zip/plugin-check.zip + if-no-files-found: error + retention-days: 1 + # Pinned to commits: this job holds a write token, and a moved tag on + # either action would run new code with it. - name: Expose built artifact as a public URL id: expose - uses: WordPress/action-wp-playground-pr-preview/.github/actions/expose-artifact-on-public-url@v3 + uses: WordPress/action-wp-playground-pr-preview/.github/actions/expose-artifact-on-public-url@43fc435558bc6cee69f5b214d6b8ca4f9f80c31d # v3 with: - artifact-name: plugin-check-zip - artifact-filename: plugin-check.zip - artifact-source-run-id: ${{ github.event.workflow_run.id }} - artifact-source-repository: ${{ github.repository }} - pr-number: ${{ steps.metadata.outputs.pr-number }} - commit-sha: ${{ steps.metadata.outputs.head-sha }} - artifacts-to-keep: '2' - github-token: ${{ secrets.GITHUB_TOKEN }} + artifact-name: plugin-check-zip + artifact-filename: plugin-check.zip + pr-number: ${{ steps.metadata.outputs.pr-number }} + commit-sha: ${{ github.event.workflow_run.head_sha }} + artifacts-to-keep: '2' + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Generate Playground blueprint id: blueprint @@ -127,7 +125,7 @@ jobs: NODE - name: Post Playground Preview Button - uses: WordPress/action-wp-playground-pr-preview@v3 + uses: WordPress/action-wp-playground-pr-preview@43fc435558bc6cee69f5b214d6b8ca4f9f80c31d # v3 with: mode: append-to-description blueprint: ${{ steps.blueprint.outputs.blueprint }} diff --git a/.github/workflows/pr-playground-preview.yml b/.github/workflows/pr-playground-preview.yml index 0ddd70cfe..a4ff3ceea 100644 --- a/.github/workflows/pr-playground-preview.yml +++ b/.github/workflows/pr-playground-preview.yml @@ -27,6 +27,9 @@ jobs: steps: - name: Checkout PR head uses: actions/checkout@v7 + with: + # Untrusted code: never persist credentials. + persist-credentials: false - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -36,7 +39,8 @@ jobs: coverage: none - name: Install production Composer dependencies - # --no-scripts stops a PR's composer.json from running code during the build. + # The PR controls this whole job, so --no-scripts is not a boundary; + # the read-only token and the publisher's artifact checks are. run: composer install --no-dev --optimize-autoloader --no-interaction --no-progress --no-scripts - name: Build plugin zip honouring .distignore @@ -54,12 +58,6 @@ jobs: zip -qr "${PLUGIN_SLUG}.zip" "${PLUGIN_SLUG}" ls -lh "${PLUGIN_SLUG}.zip" - - name: Write Playground preview metadata - run: | - mkdir -p build/playground-preview - printf '%s' '${{ github.event.pull_request.number }}' > build/playground-preview/pr-number - printf '%s' '${{ github.event.pull_request.head.sha }}' > build/playground-preview/head-sha - - name: Upload plugin zip artifact uses: actions/upload-artifact@v7 with: @@ -67,11 +65,3 @@ jobs: path: build/plugin-check.zip if-no-files-found: error retention-days: 5 - - - name: Upload Playground preview metadata - uses: actions/upload-artifact@v7 - with: - name: plugin-check-preview-metadata - path: build/playground-preview - if-no-files-found: error - retention-days: 5