Skip to content

About

No description, website, or topics provided.

Resources

Code of conduct

Stars

16 stars

Watchers

1 watching

Forks

Latest commit

 

History

130 Commits

Folders and files

Repository files navigation

WordPress Playground PR Preview Action

Add a Preview in WordPress Playground button to pull requests for WordPress plugins and themes. Reviewers can open the pull request in a browser-based WordPress instance and test the changes with one click.

Preview in WordPress Playground

live example · quick start · recipes · reference · troubleshooting

Upgrading from v3? v4 keeps the same direct-action and reusable build/publish setup paths. Some custom templates and legacy callers need changes. Read Upgrading from v3 before updating. v3 now emits an upgrade warning; its preview behavior is unchanged.

Start with Quick start and choose the path that matches your repository.


Table of contents


Quick start

Which setup should I use?

Your repository Use this setup
A plugin or theme works directly from the repository checkout No build step
A plugin or theme needs Composer, npm, Vite, or another build command first With a build step
You accept public fork PRs and need previews to work for those contributors With a build step
A preview should install multiple built plugins or themes Start with With a build step, then use the monorepo recipe
The repository is private The default setup is not enough; Playground needs public, unauthenticated URLs. See Limitations & gotchas.

If you are not sure, start with No build step only when the files committed to the pull request are exactly the files WordPress should run. If CI must generate anything first, use With a build step.

No build step

Use this setup when your plugin or theme can run directly from the repository, with no Composer install, npm build, or asset pipeline. Create one workflow file:

# .github/workflows/pr-preview.yml
name: PR Preview
on:
  pull_request:
    types: [opened, synchronize, reopened, edited]

jobs:
  preview:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: WordPress/action-wp-playground-pr-preview@v4
        with:
          plugin-path: .            # or: theme-path: .
          github-token: ${{ secrets.GITHUB_TOKEN }}

You do not need to create secrets.GITHUB_TOKEN; GitHub provides it automatically. The permissions block gives that token the access this action needs.

Open a pull request. The action adds a Preview button to the pull request description. When someone clicks it, Playground fetches the plugin or theme from GitHub and boots WordPress with it activated.

This direct setup does not run a build command or publish a ZIP artifact; Playground loads the files from GitHub at the pull request ref.

Expected result:

  • The PR workflow finishes successfully.
  • The PR description contains a managed Preview button block.
  • Clicking the button opens Playground with your plugin or theme installed and activated.

Fork PR note: this direct one-workflow setup is simplest for same-repository PRs. Public fork PRs usually receive a read-only GITHUB_TOKEN, so the action may be unable to edit the PR description. If fork contributors need working previews, use the two-workflow build/publish setup below even when the build command is just a small zip step.

With a build step

Use this setup when the preview needs generated files, such as Composer dependencies, npm/Vite bundles, or another build output. Create two workflow files: one to build a ZIP artifact and one to publish a Preview button after the build succeeds.

# .github/workflows/pr-preview-build.yml
name: PR Preview - Build
on:
  pull_request:
    types: [opened, synchronize, reopened, edited]

jobs:
  build:
    uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-build.yml@v4
    with:
      artifacts: my-plugin=build/my-plugin.zip
      node-version: '20'
      build-command: |
        npm ci
        npm run build:plugin-zip

In artifacts: my-plugin=build/my-plugin.zip, my-plugin is the artifact name and build/my-plugin.zip is the ZIP file your build-command must create. Wrap the files in a stable plugin folder, for example my-plugin/my-plugin.php. ZIP-root files can also install, but their directory name may be derived from the PR-specific ZIP filename. See Use a stable plugin folder inside ZIPs.

# .github/workflows/pr-preview-publish.yml
name: PR Preview - Publish
on:
  workflow_run:
    workflows: ["PR Preview - Build"]
    types: [completed]

jobs:
  publish:
    permissions:
      contents: write
      pull-requests: write
    uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-publish.yml@v4
    with:
      kind: plugin            # or: kind: theme

kind: plugin tells the publish workflow to generate the simplest Blueprint for one plugin ZIP: install it and activate it. Use kind: theme for one theme ZIP. For multiple ZIPs or extra setup steps, use a custom blueprint: recipe below.

The reusable publish workflow passes secrets.GITHUB_TOKEN to the action internally. Do not add github-token under jobs.publish.with: it is an action input, not a reusable workflow input. Neither a manually created secret nor secrets: inherit is needed for this token.

Commit the publish workflow to the repository's default branch before testing a PR. GitHub reads workflow_run workflows from that branch, not from the PR branch.

Open a pull request. The build workflow runs npm ci && npm run build:plugin-zip. After that succeeds, the publish workflow uploads the resulting ZIP to a public release URL and posts the Preview button. When someone clicks it, Playground installs and activates the built plugin.

Expected result:

  • The build workflow uploads a wp-playground-preview-pr<N>-<SHA> artifact.
  • The publish workflow creates or updates a ci-artifacts prerelease and uploads the built ZIP there.
  • The PR description contains a Preview button that opens Playground with the built plugin or theme installed.

Why two workflow files? The build workflow runs untrusted pull request code with read-only permissions. The publish workflow runs later, from the trusted default-branch workflow, with the write permissions needed to upload release assets and update the pull request. See How it works.


See it live

Each link is a real, public repo running these workflows. Each PR has a working Preview button that boots Playground with the PR's contents. The repository names retain v3 for stable links; their workflow references select the action version.

Shape Repo Same-repo PR Fork PR
Single plugin, build step (kind: plugin) example-simple #2 #3
Monorepo, fixed activation set (blueprint: template) example-monorepo #2 #3
Monorepo, install only changed plugin (blueprint-from-artifact) example-monorepo-selective #2 #3
Plugin with PHP dependencies and built JavaScript/CSS example-composer-vite #1 #2

Recipes

Pick the recipe that matches your repository, then adjust the paths, commands, and artifact names for your project.

Unless a recipe shows a full workflow file, the YAML snippet is a replacement step under jobs.preview.steps in the no-build workflow.

Plugin in a subdirectory

In .github/workflows/pr-preview.yml, replace the Quick start step under jobs.preview.steps with:

- uses: WordPress/action-wp-playground-pr-preview@v4
  with:
    plugin-path: plugins/my-awesome-plugin
    github-token: ${{ secrets.GITHUB_TOKEN }}

Theme

In .github/workflows/pr-preview.yml, replace the Quick start step under jobs.preview.steps with:

- uses: WordPress/action-wp-playground-pr-preview@v4
  with:
    theme-path: .             # or themes/my-theme
    github-token: ${{ secrets.GITHUB_TOKEN }}

Plugin and theme together

Use this when a PR should preview a plugin and a theme from the same repository. In .github/workflows/pr-preview.yml, replace the Quick start step under jobs.preview.steps with:

- uses: WordPress/action-wp-playground-pr-preview@v4
  with:
    plugin-path: plugins/my-plugin
    theme-path:  themes/my-theme
    github-token: ${{ secrets.GITHUB_TOKEN }}

Custom blueprint (companion plugins, version pin, seed data, login)

When you need more than "install this plugin," provide a full Blueprint via blueprint:. Example: install your plugin from the PR, also install WooCommerce from .org, pin PHP, use the latest WordPress release, and log in as admin.

- uses: WordPress/action-wp-playground-pr-preview@v4
  with:
    blueprint: |
      {
        "$schema": "https://playground.wordpress.net/blueprint-schema.json",
        "preferredVersions": { "php": "8.3", "wp": "latest" },
        "steps": [
          { "step": "installPlugin",
            "pluginData": {
              "resource": "git:directory",
              "url": "https://github.com/${{ github.repository }}.git",
              "ref": "${{ github.event.pull_request.head.ref }}",
              "path": "/"
            },
            "options": { "activate": true } },
          { "step": "installPlugin",
            "pluginData": { "resource": "wordpress.org/plugins", "slug": "woocommerce" },
            "options": { "activate": true } },
          { "step": "login", "username": "admin" }
        ]
      }
    github-token: ${{ secrets.GITHUB_TOKEN }}

Or host the blueprint elsewhere and pass the URL:

- uses: WordPress/action-wp-playground-pr-preview@v4
  with:
    blueprint-url: https://example.com/path/to/blueprint.json
    github-token: ${{ secrets.GITHUB_TOKEN }}

Learn more about Blueprints: https://wordpress.github.io/wordpress-playground/blueprints/.

Choose the Blueprint input based on where the JSON comes from:

  • Use blueprint when the workflow can include the full JSON string.
  • Use blueprint-url when the JSON is already hosted at a public URL.
  • Use blueprint-from-artifact when the build workflow generates blueprint.json dynamically.

Single plugin with a build step

See the Quick start with a build step above. The reusable workflow handles checkout, optional Node/PHP setup, build, ZIP upload, public URL, Blueprint generation, and button posting.

For predictable plugin paths, wrap the ZIP contents in a stable folder, such as my-plugin/my-plugin.php. Files at the ZIP root can install, but WordPress may name their directory after the downloaded ZIP, including its PR number and commit SHA. If your build command runs zip -r my-plugin.zip . from inside the plugin directory, stage the files into a folder first, then zip that folder.

Monorepo with multiple plugins, all activated together

Use this when one pull request preview should install more than one built plugin. The build workflow creates one ZIP per plugin; the publish workflow uses a custom Blueprint that installs both ZIPs.

Put the artifacts and build-command inputs under jobs.build.with in .github/workflows/pr-preview-build.yml:

jobs:
  build:
    uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-build.yml@v4
    with:
      artifacts: |
        site-toolkit=build/site-toolkit.zip
        site-analytics=build/site-analytics.zip
      build-command: |
        set -euo pipefail
        mkdir -p build stage
        for slug in site-toolkit site-analytics; do
          rm -rf "stage/$slug"
          mkdir -p "stage/$slug"
          rsync -a "plugins/$slug/" "stage/$slug/"
          ( cd stage && zip -rq "../build/$slug.zip" "$slug" )
        done

Put the blueprint input under jobs.publish.with in .github/workflows/pr-preview-publish.yml instead of kind: plugin:

jobs:
  publish:
    permissions:
      contents: write
      pull-requests: write
    uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-publish.yml@v4
    with:
      blueprint: |
        {
          "$schema": "https://playground.wordpress.net/blueprint-schema.json",
          "steps": [
            { "step": "installPlugin",
              "pluginData": { "resource": "url", "url": "{{ARTIFACT_URL:site-toolkit}}" },
              "options": { "activate": true } },
            { "step": "installPlugin",
              "pluginData": { "resource": "url", "url": "{{ARTIFACT_URL:site-analytics}}" },
              "options": { "activate": true } }
          ]
        }

{{ARTIFACT_URL:<name>}} is replaced with the public URL of the matching ZIP. The <name> must match the left side of the corresponding artifacts entry, such as site-toolkit.

Live: example-monorepo.

Monorepo, install only the plugin touched by the PR

The build script computes the diff against the base ref and writes a tailored blueprint.json. The publish workflow reads that Blueprint from the artifact bundle.

Put these inputs under jobs.build.with in the build workflow:

fetch-depth: 0          # so `git diff` against base ref works
artifacts: |
  alpha=build/alpha.zip
  beta=build/beta.zip
blueprint-from-build: blueprint.json
build-command: |
  set -euo pipefail
  mkdir -p build stage
  for slug in alpha beta; do
    rm -rf "stage/$slug"
    mkdir -p "stage/$slug"
    rsync -a "plugins/$slug/" "stage/$slug/"
    ( cd stage && zip -rq "../build/$slug.zip" "$slug" )
  done
  git fetch --no-tags --depth=50 origin "$GITHUB_BASE_REF"
  changed=$(git diff --name-only "origin/$GITHUB_BASE_REF...HEAD" \
    | awk -F/ '/^plugins\// {print $2}' | sort -u)
  node - "$changed" <<'NODE' > blueprint.json
  const slugs = (process.argv[2] || '').split(/\s+/).filter(Boolean);
  process.stdout.write(JSON.stringify({
    $schema: 'https://playground.wordpress.net/blueprint-schema.json',
    steps: (slugs.length ? slugs : ['alpha', 'beta']).map(s => ({
      step: 'installPlugin',
      pluginData: { resource: 'url', url: '{{ARTIFACT_URL:' + s + '}}' },
      options: { activate: true },
    })),
  }));
  NODE

Put this under jobs.publish.with in the publish workflow:

blueprint-from-artifact: true

Live: example-monorepo-selective. PR description blueprints decode to install only the plugin(s) the PR touched.

Plugin with PHP dependencies and built JavaScript/CSS

Use this when the plugin needs both PHP dependencies and compiled front-end assets before Playground can run it. In this example, Composer installs production PHP dependencies, npm/Vite builds JavaScript and CSS, and the build workflow zips the finished plugin.

Put these inputs under jobs.build.with in the build workflow:

artifacts: my-plugin=build/my-plugin.zip
node-version: '20'
php-version: '8.2'
build-command: |
  set -euo pipefail

  # 1. PHP runtime deps only.
  composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader

  # 2. Vite bundle.
  npm ci
  npm run build

  # 3. Stage a slug-named directory so the zip extracts to
  #    wp-content/plugins/my-plugin/.
  mkdir -p stage/my-plugin build
  rsync -a \
    --exclude '.git' --exclude '.github' \
    --exclude 'node_modules' --exclude 'src-js' \
    --exclude 'vite.config.js' --exclude 'package*.json' \
    --exclude 'composer*.json' --exclude '.gitignore' \
    --exclude 'stage' --exclude 'build' \
    ./ stage/my-plugin/
  ( cd stage && zip -rq ../build/my-plugin.zip my-plugin )

The publish workflow can stay at kind: plugin because there is still only one plugin ZIP to install. Live: example-composer-vite.

Built plugin with a custom Playground setup

Use this when a built plugin needs more than the default "install and activate" preview. Common reasons include opening a specific admin page, logging in automatically, or adding setup steps before the reviewer starts testing.

The crux is that forked pull requests cannot safely run with write permissions. The build workflow runs untrusted code with read-only permissions and uploads only a ZIP artifact. The publish workflow runs later with write permissions, never checks out the pull request code, and substitutes the artifact URL into a Blueprint.

This recipe runs Composer and uses .distignore to exclude files from the ZIP. If your repository has no .distignore, remove --exclude-from='.distignore'.

Build workflow example:

# .github/workflows/pr-playground-preview-build.yml
name: PR Playground Preview Build
on:
  pull_request:
    types: [opened, synchronize, reopened, edited]

jobs:
  build:
    uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-build.yml@v4
    with:
      php-version: '8.1'
      artifacts: my-plugin=build/my-plugin.zip
      build-command: |
        set -euo pipefail
        composer install --no-dev --optimize-autoloader --no-interaction --no-progress
        mkdir -p build/my-plugin
        rsync -a --delete \
          --exclude-from='.distignore' \
          --exclude='.git' \
          --exclude='.github' \
          --exclude='build' \
          ./ build/my-plugin/
        ( cd build && zip -qr my-plugin.zip my-plugin )

Publish workflow example:

# .github/workflows/pr-playground-preview-publish.yml
name: PR Playground Preview Publish
on:
  workflow_run:
    workflows: ["PR Playground Preview Build"]
    types: [completed]

jobs:
  publish:
    permissions:
      contents: write
      pull-requests: write
    uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-publish.yml@v4
    with:
      blueprint: |
        {
          "$schema": "https://playground.wordpress.net/blueprint-schema.json",
          "landingPage": "/wp-admin/admin.php?page=my-plugin",
          "steps": [
            { "step": "login", "username": "admin" },
            { "step": "installPlugin",
              "pluginData": { "resource": "url", "url": "{{ARTIFACT_URL:my-plugin}}" },
              "options": { "activate": true } }
          ]
        }

The important connection is the artifact name. artifacts: my-plugin=... in the build workflow creates an artifact URL placeholder named {{ARTIFACT_URL:my-plugin}} for the publish workflow. Use that placeholder anywhere a Blueprint needs the public ZIP URL.

Post the button as a comment instead of editing the description

- uses: WordPress/action-wp-playground-pr-preview@v4
  with:
    plugin-path: .
    mode: comment
    github-token: ${{ secrets.GITHUB_TOKEN }}

In the build-step setup, pass mode: comment to preview-publish.yml instead.

Comment mode reuses a preview comment only when its marker and author match the account behind github-token. A marker in someone else's comment is ignored. Changing the token to a different account creates a new preview comment; it does not edit the previous account's comment.

Customize the button text or add testing instructions

- uses: WordPress/action-wp-playground-pr-preview@v4
  with:
    plugin-path: .
    description-template: |
      ### Test this PR in WordPress Playground

      {{PLAYGROUND_BUTTON}}

      **Branch:** {{PR_HEAD_REF}}  ·  **Plugin:** `{{PLUGIN_SLUG}}`
    github-token: ${{ secrets.GITHUB_TOKEN }}

Or for comment mode:

- uses: WordPress/action-wp-playground-pr-preview@v4
  with:
    plugin-path: .
    mode: comment
    comment-template: |
      ## Preview Changes in WordPress Playground

      {{PLAYGROUND_BUTTON}}

      ### Testing checklist
      1. Click the button.
      2. Go to Plugins → Installed Plugins.
      3. Verify `{{PLUGIN_SLUG}}` is active.

      **PR:** #{{PR_NUMBER}} — {{PR_TITLE}}
    github-token: ${{ secrets.GITHUB_TOKEN }}

Available template variables are listed under Reference → Template variables.


Using an LLM to add this to your repository

If you ask an LLM or coding agent to set this up, give it enough context to choose the right path instead of blindly pasting the first YAML snippet.

Suggested prompt:

Add WordPress/action-wp-playground-pr-preview@v4 to this repository.
First inspect whether the WordPress plugin or theme can run directly from the repository checkout, or whether CI must build files first.
If no build step is needed, add one pull_request workflow using plugin-path or theme-path.
If a build step is needed, add the two-workflow preview-build.yml / preview-publish.yml setup.
Use the smallest working configuration. Preserve existing CI. Do not use pull_request_target.
After editing, verify that any artifacts path listed in artifacts: is actually created by build-command.

Checklist for reviewing the LLM's output:

  • secrets.GITHUB_TOKEN is referenced but not created manually.
  • Direct action usage appears under jobs.<job_id>.steps[].uses.
  • Reusable workflow usage appears under jobs.<job_id>.uses.
  • Build and publish workflows use the same version, for example @v4.
  • Every artifacts entry has the form name=path/to/file.zip, and the build command creates that exact ZIP path.
  • Plugin ZIPs wrap their files in a stable folder so installed paths do not depend on the PR-specific ZIP filename.
  • The workflow does not use pull_request_target.

How it works

Mental model

Playground runs WordPress entirely in the browser via WebAssembly. Anything Playground needs to install — a plugin ZIP, a theme ZIP, a WXR file — must be reachable through a public URL when the button is clicked. This action builds the right Blueprint and publishes a button to that Blueprint on the pull request.

The action uses one of two public URL strategies:

  1. git:directory — Playground fetches the repository at a specific ref directly from GitHub. This is the direct no-build setup.
  2. A release-asset URL — CI builds a ZIP and uploads it to a release in the repository. Use this when the preview needs Composer dependencies, npm output, compiled assets, or any other generated files.

The direct action inputs handle the git:directory path. The two reusable workflows handle the release-asset path end to end: they run the build, upload the result to a ci-artifacts prerelease, and call the action with a Blueprint that points at the resulting URL.

Fork safety model (build path only)

GitHub doesn't let one workflow simultaneously (a) run untrusted code from a fork PR and (b) write to releases or PR comments. The build path therefore splits the work at the artifact boundary:

  • Build workflow runs on pull_request, permissions: contents: read. It checks out the pull_request ref, runs your build-command, validates that the expected zip(s) exist, logs unzip -l for inspection, and uploads a single bundle artifact. It has no secrets and does not persist checkout credentials.
  • Publish workflow runs on workflow_run, permissions: contents: write
    • pull-requests: write. It never checks out PR code. GitHub reads this workflow from the default branch, so a fork PR cannot change the privileged publish logic in the same PR.
  • Artifact bundle is the only handoff from untrusted to trusted code. The publish workflow treats it as opaque bytes: it uploads the zip(s), substitutes their URLs into a Blueprint, and lets Playground run them later inside its browser sandbox.

The publish workflow has a runtime guard that fails loudly if invoked from any trigger other than workflow_run. Non-PR source runs and failed build runs skip intentionally because there is no successful PR preview to publish. Misconfigured callers (for example someone reaches for pull_request_target) get a red failure instead of a silent skip.

Because the publish workflow is privileged, its third-party action references are pinned to commit SHAs. This avoids granting write permissions to a moved major-version tag. The internal button action is also called through an immutable commit, rather than a moving version tag.

Trigger model and security

Step Where it runs Trust
actions/checkout of the pull_request ref Build workflow (pull_request) Untrusted — no secrets, read-only
Your build-command: (composer/npm/etc.) Build workflow Untrusted — runs in CI, output (the zip) is the only thing that escapes
actions/upload-artifact of the bundle Build workflow Untrusted — bundle is opaque to the publish workflow; publish verifies the artifact PR number and SHA against the workflow_run payload and base-repo PR API before using it
Reading the bundle, exposing it on a release Publish workflow (workflow_run) Trusted — workflow YAML from default branch, never checks out PR code
Posting the Preview button on the PR Publish workflow → action Trusted
Clicking the button → Playground in the user's browser The user's browser Untrusted code, but iframe-isolated by Playground

In other words, the publish workflow does not execute pull request code. It treats the ZIP as data until a reviewer opens it in the Playground iframe, where WordPress runs inside Playground's browser sandbox.

For public repositories, release assets are public. A fork PR can therefore cause its built zip to be hosted on the repository's ci-artifacts prerelease until cleanup removes it. That is the tradeoff that makes one-click browser previews possible for fork contributors.

{{ARTIFACT_URL:<name>}} substitution uses JSON.stringify(url).slice(1, -1), so any character that could break JSON parsing is escaped. The "{{...}}" template convention is non-breaking and produces valid JSON for any URL.

The ci-artifacts release is created as a --prerelease, not a draft. Prerelease assets are publicly downloadable on first run; draft assets require auth and Playground can't read them. The action handles this for you on first use; existing draft releases need a one-time conversion (see Migrating).


Reference

Action: WordPress/action-wp-playground-pr-preview@v4

Use directly when there's no build step, or have the publish workflow call it (it does, internally).

Input Required Default Description
mode no append-to-description append-to-description or comment.
playground-host no https://playground.wordpress.net Base Playground host URL.
plugin-path one of four† — Path to plugin directory. . for repo root, plugins/foo for subdir. Auto-generates a git:directory blueprint.
theme-path one of four† — Path to theme directory. Auto-generates a git:directory blueprint.
blueprint one of four† — Custom Blueprint as a JSON string. When set, plugin-path and theme-path are ignored.
blueprint-url one of four† — URL pointing to a hosted Blueprint JSON. Used directly via ?blueprint-url=….
description-template no {{PLAYGROUND_BUTTON}} Template for the PR description block. Supports the template variables.
comment-template no (full default) Template for the PR comment. Supports the template variables.
restore-button-if-removed no true Add the button when its markers are absent. Set false to leave it absent, including on the first run. Existing managed buttons still update. Only applies to append-to-description mode.
pr-number no event payload Pull request number. Required when calling from a workflow that doesn't have a pull_request event payload (e.g. workflow_run).
github-token yes — Token with pull-requests: write and contents: read, usually ${{ secrets.GITHUB_TOKEN }}.

† Provide blueprint-url, blueprint, or one/both of plugin-path and theme-path. Do not combine blueprint-url or blueprint with path inputs unless you intentionally want the custom Blueprint/URL to win.

Outputs

Output Description
preview-url Full Playground URL embedded in the button.
blueprint-json Blueprint JSON from blueprint or the path inputs. Empty when only blueprint-url is provided.
rendered-description Rendered description template, available in either mode even if the description was not updated.
rendered-comment Rendered comment template, available in either mode even if no comment was posted.
mode Effective mode (append-to-description or comment).
comment-id ID of the managed PR comment, when applicable.

Reusable workflow: preview-build.yml@v4

Runs the caller's build command in the read-only pull_request context and bundles the produced zip(s) into a single GitHub Actions artifact for the publish workflow to consume.

Input Required Default Description
artifacts yes — Newline-separated name=path entries. name becomes the slug used in {{ARTIFACT_URL:<name>}} and must match [a-zA-Z0-9_-]+. path is relative to working-directory.
build-command yes — Shell script that produces every path listed in artifacts. Runs in bash; set -euo pipefail-style strictness recommended.
working-directory no . Working directory for build-command.
node-version no unset If set, runs actions/setup-node@v4 before build-command.
php-version no unset If set, runs shivammathur/setup-php@v2 before build-command. This selects the build's PHP version, not Playground's; use preferredVersions.php in a custom Blueprint for that.
fetch-depth no 1 Passed to actions/checkout@v4. Set to 0 when the build needs full history (e.g. diff against the base ref).
blueprint-from-build no unset Path (relative to working-directory) to a blueprint.json written by build-command. Bundled with the artifact for use with blueprint-from-artifact: true in publish. Validated as parseable JSON before upload.

The bundle artifact is named wp-playground-preview-pr<N>-<SHA> and contains zips/<name>.zip per artifacts entry plus optional blueprint.json.

Reusable workflow: preview-publish.yml@v4

Runs in the privileged workflow_run context, exposes the artifact bundle's zips on a public release URL, renders the Blueprint, and posts the Preview button.

Input Required Default Description
blueprint one of three‡ — Blueprint JSON template. Use {{ARTIFACT_URL:<name>}} placeholders inside double quotes.
kind one of three‡ — plugin or theme. Shortcut: requires exactly one zip in the bundle, generates an installPlugin/installTheme step with activate: true.
blueprint-from-artifact one of three‡ false When true, read blueprint.json from the artifact bundle (requires blueprint-from-build: on the build side).
artifacts-to-keep no 2 Positive integer number of distinct PR commits worth of zips to keep on the release. Older zips for the same PR get pruned. Set to keep-all to disable cleanup.
release-tag no ci-artifacts Tag used to host artifacts publicly. Auto-created as a prerelease on first use.
mode no append-to-description append-to-description or comment.

‡ Provide exactly one of blueprint, kind, blueprint-from-artifact. The publish workflow validates this and fails loudly if zero or two are set.

Required caller permissions

The job that calls preview-publish.yml must grant:

permissions:
  contents: write
  pull-requests: write

Put this block under jobs.publish, as in the quick start. Alternatively, set it at the workflow level and let the job inherit it. You do not need both: a job-level block overrides the workflow-level defaults. See GitHub's permission rules.

Without these permissions, GitHub may fail the run at startup before the job logs are available. See Troubleshooting.

Template variables

Available in description-template and comment-template strings (case-insensitive {{NAME}} syntax):

Variable Value
PLAYGROUND_BUTTON Full button HTML — recommended in any custom template.
PLAYGROUND_URL Full Playground URL with embedded blueprint.
PLAYGROUND_BUTTON_IMAGE_URL URL of the button image asset.
PLAYGROUND_BLUEPRINT_JSON Blueprint JSON from blueprint or the path inputs. Empty when only blueprint-url is provided.
PLAYGROUND_BLUEPRINT_DATA_URL Blueprint data URL, or the provided blueprint-url when blueprint-url is used.
PLAYGROUND_HOST Playground host (default https://playground.wordpress.net).
PR_NUMBER, PR_TITLE, PR_HEAD_REF, PR_HEAD_SHA, PR_BASE_REF Pull request metadata.
REPO_OWNER, REPO_NAME, REPO_FULL_NAME, REPO_SLUG, REPO_ARCHIVE_ROOT Repository metadata.
PLUGIN_PATH, PLUGIN_SLUG Set when plugin-path: is provided.
THEME_PATH, THEME_SLUG Set when theme-path: is provided.

PR_* and REPO_* values are inserted as text: spaces and punctuation are encoded as character references and line breaks become spaces, so a PR title such as [text](url) stays literal rather than becoming a link. Use these placeholders in normal Markdown text or HTML, not inside Markdown code spans or fenced code blocks, which display character references verbatim. Templates still control their own links and formatting. Other values remain HTML-escaped, except PLAYGROUND_BUTTON, which contains the generated button HTML.


Limitations & gotchas

  • Two workflow files when there's a build step. GitHub's permission model around fork PRs makes this unavoidable. The reusable workflows minimise but don't eliminate the boilerplate.

  • The caller sets the available permissions. A reusable workflow can keep or reduce the permissions passed by its caller, but cannot add missing permissions. The publish caller must allow contents: write and pull-requests: write.

  • Build and publish workflows must be pinned to compatible versions. The artifact-naming format is the implicit interface between them. Use the same @v4 (or branch ref) in both.

  • Fork PR build output becomes public. The publish workflow never trusts the zip, but it does upload it to a public release URL so Playground can fetch it. Keep artifacts-to-keep low unless you deliberately want longer retention.

  • Artifact Blueprint templates support only {{ARTIFACT_URL:<name>}} substitution. No conditionals, loops, or other placeholders. For per-PR variable shapes, write the blueprint at build time and use blueprint-from-artifact: true. Description and comment templates have their own template variables.

  • One zip per artifacts entry. Use multiple entries plus a custom blueprint: for multiple plugin/theme zips; the kind: shortcut is only for a single zip.

  • Use a stable plugin folder inside ZIPs. Package my-plugin/my-plugin.php to keep the installed path predictable. ZIP-root files can install too, but their directory name may come from the PR-specific ZIP filename. From the plugin repository root, stage the files before zipping. Exclude the staging and output directories so they are not copied into themselves:

    mkdir -p stage/my-plugin build
    rsync -a --exclude='.git' --exclude='.github' \
      --exclude='stage' --exclude='build' ./ stage/my-plugin/
    ( cd stage && zip -rq ../build/my-plugin.zip my-plugin )

    Add any project-specific exclusions, such as node_modules, before using this in your build.

  • Base-ref diffs need enough history. The default checkout is shallow (depth 1). Set fetch-depth: 0 to fetch full history and avoid missing merge bases. If you use a shallow fetch instead, it must include the PR's merge base.

  • The ci-artifacts release is shared across all PRs. Each PR's zips are unique (pr-<N>-<SHA>-<name>.zip); cleanup keeps the N most recent commit-sets per PR.

  • artifacts-to-keep must be a positive integer or keep-all. 0, negative numbers, and arbitrary strings fail before any release assets are uploaded.

  • workflow_run-triggered workflows always read their YAML from the default branch. Workflow changes on a PR branch don't take effect until merged. Test publish-side changes on a scratch repo first.

  • Private repositories are not supported by the default setup. Playground runs in the user's browser and needs unauthenticated download URLs. git:directory and release assets in private repositories both require authentication that Playground does not have. Make the repository public or self-host the ZIP.


Troubleshooting

The publish workflow run is startup_failure with no logs

Check the workflow annotation for the cause. If it says the called workflow requests permissions that are not allowed, grant contents: write and pull-requests: write on the calling job:

jobs:
  publish:
    permissions:
      contents: write
      pull-requests: write
    uses: WordPress/action-wp-playground-pr-preview/.github/workflows/preview-publish.yml@v4
    # ...

The Preview button 404s when clicked

Either the artifact was not uploaded, or the release is a draft and requires authentication. Check the ci-artifacts release in the repository's Releases page. If it is a draft, convert it to a prerelease. The action does this automatically for new releases, but it does not change an existing draft release.

My plugin needs composer install or npm run build and shows up empty

The workflow is probably using plugin-path: on the action directly. That path uses git:directory, so Playground receives the repository files without running a build step. Switch to the build-step setup so composer install, npm ci, or other build commands run in CI before zipping.

git diff origin/$GITHUB_BASE_REF...HEAD fails with "no merge base"

The default actions/checkout is shallow. Set fetch-depth: 0 on the build reusable workflow input.

PHP.run() failed with exit code 255 and no stderr

Usually a fatal in plugin activation — most often a missing vendor/autoload.php because Composer wasn't run before zipping. Switch to the build-step setup or check what your build-command actually produces. The build workflow logs unzip -l of the final artifact for exactly this reason.

The Preview button gets re-added after I delete it

That's restore-button-if-removed: true (the default). Set it to false to skip insertion whenever the markers are absent. This also prevents the first insertion on a new PR: the action cannot tell a deleted block from one it never added.

To hide the button on just one PR while still adding it to new PRs, leave the default enabled and replace the button block with a placeholder:

<!-- wp-playground-preview:start -->
<!-- Preview button hidden by PR author -->
<!-- wp-playground-preview:end -->

Resource not accessible by integration

Check that the calling job grants pull-requests: write and the direct action step passes github-token. For a public fork PR, GitHub usually reduces GITHUB_TOKEN to read-only even when the workflow requests write access. Use the two-workflow setup for those PRs.

The reusable workflow uses: line fails YAML lint

WordPress/action-wp-playground-pr-preview/.github/workflows/preview-{build,publish}.yml@v4 is a reusable workflow path, distinct from WordPress/action-wp-playground-pr-preview@v4 which is the action. Both are valid; use them in the right place. Reusable workflows go under jobs.<id>.uses. Actions go under jobs.<id>.steps[].uses.


Migrating from older usage

Upgrading from v3

Change direct action references from @v3 to @v4. For built previews, change both preview-build.yml and preview-publish.yml references to @v4. The existing inputs and build commands still apply. Keep the publish caller on the default branch.

Check these differences before upgrading:

  • Custom templates: put PR_* and REPO_* placeholders outside Markdown code spans or fenced blocks. For example, replace **Branch:** `{{PR_HEAD_REF}}` with **Branch:** {{PR_HEAD_REF}}. These values now render as literal text, including punctuation in titles and branch names.
  • Comment mode: existing comments are reused only when their marker and author match the account behind github-token. Switching accounts creates a new comment. A failed account lookup stops the update.
  • Artifact bundles: use ordinary files and directories, not links or special files. The legacy helper now copies only the requested artifact-filename into a temporary directory; other bundle files are no longer extracted into the caller's workspace.
  • Legacy workflow-run callers: pass the source run ID and its head_sha, and obtain the PR number from GitHub's event or API. The run must match the PR's current repository, branch, and commit. Use positive, unpadded run and PR numbers. The token needs actions: read and pull-requests: read in the source repository, plus contents: write where the release is published. The source lookup is described below.
  • Release names: the reusable publisher uses one PR number spelling for uploads and cleanup (007 becomes 7). Previously uploaded assets are not renamed.

The v3 tag now includes an upgrade warning in the direct action, both reusable workflows, and the legacy artifact helper. The warning does not fail the job or change preview behavior. The v2 tag and callers pinned to older commit hashes are unchanged; those callers do not receive the warning.

v3 is deprecated. Use v4 for new setups and migrate existing callers using the steps above. No v3 support end date has been announced.

Migrating pre-v3 workflows

The common pre-v3 advanced pattern required a long custom YAML setup across two workflow files: GitHub Script for parsing artifact metadata, a Node heredoc for building the Blueprint, and a manual one-time step to publish a draft release. The reusable workflows now handle those details. If you used less-common expose-artifact-on-public-url inputs such as artifact-source-repository, release-repository, create-release-if-missing, or cleanup-enabled, use the legacy helper with source-run checks or wrap the reusable workflow for those options. Update older pinned helper revisions before keeping a custom publish workflow.

For legacy workflow_run callers, pass artifact-source-run-id: ${{ github.event.workflow_run.id }} and set commit-sha from that run's head_sha. Get pr-number from GitHub's event or API data, never from an artifact name or file. Use the PR number in workflow_run.pull_requests when that list identifies one PR. Fork runs can have an empty list; in that case, query the source repository's pulls API with head=OWNER:BRANCH and state=all, using workflow_run.head_repository.owner.login and workflow_run.head_branch. Follow all pages and match head.repo.full_name and head.ref to the run. Require exactly one matching PR, then check its current head.sha. A commit hash alone can match several forks or PRs.

The helper reads both the run and PR from artifact-source-repository (the caller repository by default). Before downloading or changing a release, it requires a pull_request run with the same head repository, branch, and SHA as the PR, and requires commit-sha to match that SHA. It also checks the run's PR list, or performs the branch lookup above when the list is empty. A stale run, mismatched PR, or ambiguous branch fails without uploading or pruning assets. The token must be able to read Actions runs and pull requests in the source repository. release-repository only selects where the release is stored.

To migrate:

  1. Replace your build workflow. Move whatever it ran (composer install, npm ci, etc.) into the build-command: input of preview-build.yml@v4. Replace actions/upload-artifact@v4 with name=path lines in artifacts:.
  2. Replace your publish workflow. Pick a blueprint mode: kind: for a single zip, blueprint: for fixed shapes, blueprint-from-artifact: for per-PR shapes. Add the permissions: block on the calling job and commit the publish workflow to the default branch.
  3. One-time: if you have an existing ci-artifacts draft release, either delete it (the next run creates a fresh prerelease automatically) or convert it from draft to prerelease in the Releases UI. Draft release assets require authentication, so Playground cannot download them.

Older README content is preserved in git history, including the legacy github-proxy.com URL scheme. Use git log -- README.md for historical reference. Do not copy the pre-v3 artifact-name parsing recipe into a new publish workflow; use the source-run checks described above or migrate to the reusable workflows.


Contributing

Issues and PRs welcome at https://github.com/WordPress/action-wp-playground-pr-preview.

Copy the fixtures from the four example repos into a disposable public repository for manual integration testing. Leave the live examples unchanged. In the copies, replace the action and reusable workflow references with your fork and the full commit SHA being tested; use the same SHA for build and publish. The WordPress/...@v4 references above use the upstream release, not changes merged only into a fork.

Commit the test publish workflow to the disposable repository's default branch, then open a PR there. Confirm that the build uploads its ZIP, the publish job posts a button, and Playground activates the expected plugin or theme. Test description and comment modes separately.

License

GPL-2.0-or-later. See LICENSE.

About

No description, website, or topics provided.

Resources

Code of conduct

Stars

16 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages