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.
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.
- Quick start
- See it live
- Recipes
- Using an LLM to add this to your repository
- How it works
- Reference
- Limitations & gotchas
- Troubleshooting
- Migrating from older usage
- Contributing
- License
| 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.
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.
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-zipIn 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: themekind: 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-artifactsprerelease 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.
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 |
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.
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 }}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 }}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 }}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
blueprintwhen the workflow can include the full JSON string. - Use
blueprint-urlwhen the JSON is already hosted at a public URL. - Use
blueprint-from-artifactwhen the build workflow generatesblueprint.jsondynamically.
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.
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" )
donePut 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.
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 },
})),
}));
NODEPut this under jobs.publish.with in the publish workflow:
blueprint-from-artifact: trueLive: example-monorepo-selective. PR description blueprints decode to install only the plugin(s) the PR touched.
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.
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.
- 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.
- 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.
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_TOKENis 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
artifactsentry has the formname=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.
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:
git:directory— Playground fetches the repository at a specific ref directly from GitHub. This is the direct no-build setup.- 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.
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 thepull_requestref, runs yourbuild-command, validates that the expected zip(s) exist, logsunzip -lfor 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: writepull-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.
| 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).
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.
| 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. |
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.
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.
The job that calls preview-publish.yml must grant:
permissions:
contents: write
pull-requests: writePut 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.
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.
-
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: writeandpull-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-keeplow 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 useblueprint-from-artifact: true. Description and comment templates have their own template variables. -
One zip per
artifactsentry. Use multiple entries plus a customblueprint:for multiple plugin/theme zips; thekind:shortcut is only for a single zip. -
Use a stable plugin folder inside ZIPs. Package
my-plugin/my-plugin.phpto 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: 0to 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-artifactsrelease 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-keepmust be a positive integer orkeep-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:directoryand release assets in private repositories both require authentication that Playground does not have. Make the repository public or self-host the ZIP.
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
# ...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.
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.
The default actions/checkout is shallow. Set fetch-depth: 0 on the build reusable workflow input.
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.
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 -->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.
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.
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_*andREPO_*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-filenameinto 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 needsactions: readandpull-requests: readin the source repository, pluscontents: writewhere the release is published. The source lookup is described below. - Release names: the reusable publisher uses one PR number spelling for uploads and cleanup (
007becomes7). 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.
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:
- Replace your build workflow. Move whatever it ran (
composer install,npm ci, etc.) into thebuild-command:input ofpreview-build.yml@v4. Replaceactions/upload-artifact@v4withname=pathlines inartifacts:. - 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 thepermissions:block on the calling job and commit the publish workflow to the default branch. - One-time: if you have an existing
ci-artifactsdraft 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.
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.
GPL-2.0-or-later. See LICENSE.