diff --git a/.github/workflows/playground-preview-button.svg b/.github/workflows/playground-preview-button.svg new file mode 100644 index 0000000000..5d3d40ff5e --- /dev/null +++ b/.github/workflows/playground-preview-button.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/.github/workflows/playground-preview.yml b/.github/workflows/playground-preview.yml new file mode 100644 index 0000000000..ff079b0651 --- /dev/null +++ b/.github/workflows/playground-preview.yml @@ -0,0 +1,747 @@ +name: WordPress Playground Preview +description: | + # WordPress Playground Preview Button + + Automatically adds a "Try it in WordPress Playground" preview button to your pull requests, + enabling easy testing and feedback for WordPress plugins and themes. + + For usage instructions, see the [documentation page](http://wordpress.github.io/wordpress-playground/guides/github-pr-previews). + +on: + workflow_call: + inputs: + mode: + type: string + description: | + How to publish the preview button. + + Accepted values: + - `append-to-description` (default) – Automatically updates the PR description with a managed block + containing the preview button. The block is wrapped in HTML comment markers ( + and ) so it can be updated on subsequent workflow runs. + - `post-comment` – Posts the preview button as a PR comment. Updates the same comment on subsequent runs + rather than creating duplicates. + default: append-to-description + playground-host: + type: string + description: | + Base WordPress Playground host URL used to build the preview link. + + The workflow appends blueprint parameters to this URL to create the final preview link. + + Default: `https://playground.wordpress.net` + default: https://playground.wordpress.net + blueprint: + type: string + description: | + Custom WordPress Blueprint as a JSON string. + + When provided, this blueprint is used as-is and the `plugin-path` and `theme-path` inputs are ignored. + If omitted, the workflow automatically generates a blueprint based on `plugin-path` or `theme-path`. + + The blueprint must be a complete, ready-to-use JSON object (not a template). It will be URL-encoded + and passed to Playground via the `blueprint-url` parameter. + + Learn more about blueprints: https://wordpress.github.io/wordpress-playground/blueprints/ + + Example (custom blueprint with specific WordPress version): + ```yaml + with: + blueprint: | + { + "$schema": "https://playground.wordpress.net/blueprint-schema.json", + "preferredVersions": { + "php": "8.3", + "wp": "6.4" + }, + "steps": [ + { + "step": "installPlugin", + "pluginData": { + "resource": "git:directory", + "url": "https://github.com/owner/repo.git", + "ref": "feature-branch", + "path": "my-plugin" + }, + "options": { "activate": true } + } + ] + } + ``` + + You will most likely want to include the repo URL and the current branch name in a custom Blueprint. + The recommended way to do this is with a dependent job that produces a valid blueprint JSON string. + For example: + + ```yaml + name: PR Playground Preview + on: + pull_request: + types: + - opened + - synchronize + - reopened + - edited + + jobs: + create-blueprint: + name: Create Blueprint + runs-on: ubuntu-latest + outputs: + blueprint: $\{{ steps.blueprint.outputs.result }} + steps: + - name: Create Blueprint + id: blueprint + uses: actions/github-script@v7 + with: + script: | + const blueprint = { + steps: [ + { + step: "installPlugin", + pluginData: { + resource: "git:directory", + url: `https://github.com/${context.repo.owner}/${context.repo.repo}.git`, + ref: context.payload.pull_request.head.ref, + path: "/" + } + } + ] + }; + return JSON.stringify(blueprint); + result-encoding: string + + playground-preview: + name: Post Playground Preview Button + # This ensures the create-blueprint job runs before this one and + # creates a Blueprint JSON string this job can reuse: + needs: create-blueprint + + permissions: + contents: read + pull-requests: write + issues: write + uses: WordPress/wordpress-playground/.github/workflows/playground-preview-button.yml@trunk + with: + mode: "append-to-description" + blueprint: $\{{ needs.create-blueprint.outputs.blueprint }} + ``` + plugin-path: + type: string + description: | + Installs and activates a plugin from a path inside the repository. + + This is a shortcut for plugins that don't need any bundling and can + be installed directly from the repository. + + The path string should point to a directory containing a valid WordPress + plugin with a main plugin file. + + This option is ignored if the `blueprint` input is provided. + + Example (plugin in repository root): + ```yaml + with: + plugin-path: . + ``` + + Example (plugin in subdirectory): + ```yaml + with: + plugin-path: plugins/my-awesome-plugin + ``` + theme-path: + type: string + description: | + Installs and activates a theme from a path inside the repository. + + The path string should point to a directory containing a valid WordPress + theme with a style.css file. + + This option is ignored if the `blueprint` input is provided. + + Example (theme in repository root): + ```yaml + with: + theme-path: . + ``` + + Example (theme in subdirectory): + ```yaml + with: + theme-path: themes/my-cool-theme + ``` + + Example (testing theme + plugin): + ```yaml + with: + plugin-path: plugins/my-plugin + theme-path: themes/my-theme + ``` + description-template: + type: string + description: | + Custom markdown/HTML template for the content added to PR descriptions (only used in `append-to-description` mode). + + The template supports variable interpolation using {{VARIABLE_NAME}} syntax (case-insensitive). + The rendered content will be wrapped in HTML comment markers so it can be updated on subsequent runs. + + Available template variables: + • {{PLAYGROUND_BUTTON}} - Rendered preview button HTML (recommended to include) + • {{PLAYGROUND_URL}} - Full URL to the Playground preview + • {{PLAYGROUND_BUTTON_IMAGE_URL}} - URL to the button image + • {{PLAYGROUND_BLUEPRINT_JSON}} - Complete blueprint JSON string + • {{PLAYGROUND_BLUEPRINT_DATA_URL}} - Data URL containing the blueprint + • {{PLAYGROUND_HOST}} - Playground host URL + • {{PR_NUMBER}} - Pull request number + • {{PR_TITLE}} - Pull request title + • {{PR_HEAD_REF}} - Source branch name + • {{PR_HEAD_SHA}} - Latest commit SHA + • {{PR_BASE_REF}} - Target branch name + • {{REPO_OWNER}} - Repository owner username/org + • {{REPO_NAME}} - Repository name + • {{REPO_FULL_NAME}} - Full repository name (owner/repo) + • {{REPO_SLUG}} - Sanitized repository name + • {{PLUGIN_PATH}} - Plugin path (if provided) + • {{PLUGIN_SLUG}} - Derived plugin slug + • {{THEME_PATH}} - Theme path (if provided) + • {{THEME_SLUG}} - Derived theme slug + + Default template: + ``` + {{PLAYGROUND_BUTTON}} + ``` + + Example (custom template with additional context): + ```yaml + with: + description-template: | + ### Test this PR in WordPress Playground + + {{PLAYGROUND_BUTTON}} + + **Branch:** {{PR_HEAD_REF}} + **Testing:** Plugin `{{PLUGIN_SLUG}}` + ``` + comment-template: + type: string + description: | + Custom markdown/HTML template for PR comments (only used in `post-comment` mode). + + The template supports variable interpolation using {{VARIABLE_NAME}} syntax (case-insensitive). + The rendered comment will include a hidden identifier marker so it can be updated on subsequent runs. + + Available template variables: + • {{PLAYGROUND_BUTTON}} - Rendered preview button HTML (recommended to include) + • {{PLAYGROUND_URL}} - Full URL to the Playground preview + • {{PLAYGROUND_BUTTON_IMAGE_URL}} - URL to the button image + • {{PLAYGROUND_BLUEPRINT_JSON}} - Complete blueprint JSON string + • {{PLAYGROUND_BLUEPRINT_DATA_URL}} - Data URL containing the blueprint + • {{PLAYGROUND_HOST}} - Playground host URL + • {{PR_NUMBER}} - Pull request number + • {{PR_TITLE}} - Pull request title + • {{PR_HEAD_REF}} - Source branch name + • {{PR_HEAD_SHA}} - Latest commit SHA + • {{PR_BASE_REF}} - Target branch name + • {{REPO_OWNER}} - Repository owner username/org + • {{REPO_NAME}} - Repository name + • {{REPO_FULL_NAME}} - Full repository name (owner/repo) + • {{REPO_SLUG}} - Sanitized repository name + • {{PLUGIN_PATH}} - Plugin path (if provided) + • {{PLUGIN_SLUG}} - Derived plugin slug + • {{THEME_PATH}} - Theme path (if provided) + • {{THEME_SLUG}} - Derived theme slug + + Default template: + ``` + ### WordPress Playground Preview + + The changes in this pull request can previewed and tested using a WordPress Playground instance. + + {{PLAYGROUND_BUTTON}} + ``` + + Example (custom comment with testing instructions): + ```yaml + with: + mode: post-comment + comment-template: | + ## Preview Changes in WordPress Playground + + {{PLAYGROUND_BUTTON}} + + ### Testing Instructions + 1. Click the button above to open Playground + 2. Navigate to Plugins → Installed Plugins + 3. Verify that `{{PLUGIN_SLUG}}` is active + 4. Test the new functionality + + **PR:** #{{PR_NUMBER}} - {{PR_TITLE}} + ``` + restore-button-if-removed: + type: boolean + description: | + Only applies to `append-to-description` mode. + + Controls whether the preview button is automatically restored to the PR description + if removed by the PR author. + + When `true` (default): + • If PR author completely removes the button markers → workflow re-adds them on next run + • If PR author replaces button with custom placeholder → workflow respects it (does not update) + + When `false`: + • If PR author completely removes the button markers → they stay removed + • If markers exist with custom placeholder → workflow respects it (does not update) + • If markers exist with the button → workflow updates the button normally + + How PR authors can keep the button removed: + 1. Replace with placeholder (always works): + + + + + 2. Delete completely (only works when this is set to false): + Delete the entire managed block including the markers + + Example (respect when PR author removes button): + ```yaml + with: + mode: append-to-description + restore-button-if-removed: false + ``` + default: true + secrets: + github-token: + description: | + GitHub token used to update PR descriptions and post/update comments. + + If not provided, defaults to the calling workflow's `GITHUB_TOKEN` (recommended for most cases). + + Required permissions: + • `pull-requests: write` - To update PR descriptions and manage comments + • `contents: read` - To access repository information + + The default `GITHUB_TOKEN` automatically has these permissions in most workflows. + + Only provide a custom token if you need to: + • Use a fine-grained personal access token with specific permissions + • Work around workflow restrictions in your repository + + Example (using default token - recommended): + ```yaml + uses: ./.github/workflows/playground-preview.yml + with: + plugin-path: . + # No secrets needed - GITHUB_TOKEN is used automatically + ``` + + Example (using custom token): + ```yaml + uses: ./.github/workflows/playground-preview.yml + with: + plugin-path: . + secrets: + github-token: + ``` + required: false + +jobs: + build-preview: + name: Build Playground Preview + permissions: + contents: read + pull-requests: write + issues: write + runs-on: ubuntu-latest + steps: + - name: Ensure repository context + if: ${{ !github.event.pull_request }} + run: | + echo "This reusable workflow must be invoked from a pull_request triggered workflow." + exit 1 + + - name: Prepare preview content + id: preview + uses: actions/github-script@v7 + env: + INPUT_PREVIEW_MODE: ${{ inputs.mode }} + INPUT_PLAYGROUND_HOST: ${{ inputs.playground-host }} + INPUT_BLUEPRINT: ${{ inputs.blueprint }} + INPUT_PLUGIN_PATH: ${{ inputs.plugin-path }} + INPUT_THEME_PATH: ${{ inputs.theme-path }} + INPUT_DESCRIPTION_TEMPLATE: ${{ inputs.description-template }} + INPUT_COMMENT_TEMPLATE: ${{ inputs.comment-template }} + INPUT_RESTORE_BUTTON_IF_REMOVED: ${{ inputs.restore-button-if-removed }} + with: + github-token: ${{ secrets.github-token != '' && secrets.github-token || github.token }} + result-encoding: string + script: | + const mode = (process.env.INPUT_PREVIEW_MODE || 'append-to-description').trim().toLowerCase(); + if (mode !== 'append-to-description' && mode !== 'post-comment') { + throw new Error(`Invalid preview mode: ${mode}. Accepted values: append-to-description, post-comment.`); + } + + const pr = context.payload.pull_request; + if (!pr) { + throw new Error('This workflow must run on a pull_request event payload.'); + } + + const repo = context.payload.repository; + const owner = repo.owner.login || repo.owner.name || repo.owner.id; + const repoName = repo.name; + const repoFullName = repo.full_name; + const prNumber = pr.number; + const prTitle = pr.title; + const headRef = pr.head.ref; + const headSha = pr.head.sha; + const baseRef = pr.base.ref; + + const playgroundHostRaw = process.env.INPUT_PLAYGROUND_HOST || 'https://playground.wordpress.net'; + const playgroundHost = playgroundHostRaw.replace(/\/+$/, ''); + + const pluginPath = (process.env.INPUT_PLUGIN_PATH || '').trim(); + const themePath = (process.env.INPUT_THEME_PATH || '').trim(); + const blueprintInput = process.env.INPUT_BLUEPRINT || ''; + + if(!pluginPath && !themePath && !blueprintInput) { + throw new Error('One of `plugin-path`, `theme-path`, or `blueprint` inputs is required.'); + } + + const descriptionTemplateInput = process.env.INPUT_DESCRIPTION_TEMPLATE || ''; + const commentTemplateInput = process.env.INPUT_COMMENT_TEMPLATE || ''; + const descriptionMarkerStart = ''; + const descriptionMarkerEnd = ''; + const commentIdentifier = ''; + const restoreButtonIfRemoved = process.env.INPUT_RESTORE_BUTTON_IF_REMOVED !== 'false'; + + const safeParseJson = (label, value, fallback = {}) => { + if (!value || !value.trim()) { + return fallback; + } + try { + return JSON.parse(value); + } catch (error) { + throw new Error(`Unable to parse ${label} as JSON. ${error.message}`); + } + }; + + const archiveBranchSegment = headRef.replace(/[^0-9A-Za-z]/g, '-'); + const repoArchiveRoot = `${repoName}-${archiveBranchSegment}`; + const repoGitUrl = `https://github.com/${repoFullName}.git`; + + const normalizePath = (path) => { + const raw = (path || '').trim(); + if (!raw || raw === '.' || raw === './') { + return ''; + } + return raw.replace(/^\.\/+/, '').replace(/^\/+|\/+$/g, ''); + }; + const sanitizeSlug = (value, fallback) => { + if (!value) return fallback; + const cleaned = value + .toLowerCase() + .replace(/[^a-z0-9-]+/g, '-') + .replace(/^-+|-+$/g, ''); + return cleaned || fallback; + }; + const repoSlug = sanitizeSlug(repoName, 'project'); + const inferSlug = (path, fallback) => { + const clean = normalizePath(path).split('/').filter(Boolean).pop(); + if (!clean || clean === '.' || clean === '..') return fallback; + return sanitizeSlug(clean, fallback); + }; + + const pluginSlug = pluginPath ? inferSlug(pluginPath, repoSlug) : ''; + const themeSlug = themePath ? inferSlug(themePath, `${repoSlug}-theme`) : ''; + + const buildAutoBlueprint = () => { + const steps = []; + + if (pluginPath) { + steps.push( + { + step: 'installPlugin', + pluginData: { + resource: 'git:directory', + url: repoGitUrl, + ref: headRef, + path: normalizePath(pluginPath) || "/" + }, + options: { + activate: true + } + } + ); + } + + if (themePath) { + steps.push( + { + step: 'installTheme', + themeData: { + resource: 'git:directory', + url: repoGitUrl, + ref: headRef, + path: normalizePath(themePath) || "/" + }, + options: { + activate: true + } + } + ); + } + + return JSON.stringify( + { + $schema: 'https://playground.wordpress.net/blueprint-schema.json', + preferredVersions: { + php: '8.2', + wp: 'latest' + }, + steps + } + ); + }; + + const blueprintJson = blueprintInput && blueprintInput.trim().length + ? blueprintInput.trim() + : buildAutoBlueprint(); + + try { + JSON.parse(blueprintJson); + } catch (error) { + core.warning(blueprintJson); + throw new Error(`Blueprint is not valid JSON. ${error.message}`); + } + + const mergeVariables = (...maps) => maps.reduce((acc, map) => { + Object.entries(map || {}).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + acc[String(key).toUpperCase()] = typeof value === 'string' ? value : JSON.stringify(value); + }); + return acc; + }, {}); + + const substitute = (template, values) => { + if (!template) { + return ''; + } + return template.replace(/\{\{\s*([A-Z0-9_]+)\s*\}\}/gi, (match, key) => { + const upperKey = key.toUpperCase(); + let value = Object.prototype.hasOwnProperty.call(values, upperKey) + ? values[upperKey] + : ''; + + // Escape HTML entities somewhat naively to prevent the values leaking + // into HTML syntax elements. + if(key !== 'PLAYGROUND_BUTTON') { + value = value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + return value; + }); + }; + + const blueprintDataUrl = `data:application/json,${encodeURIComponent(blueprintJson)}`; + + const previewUrl = `${playgroundHost}${playgroundHost.includes('?') ? '&' : '?'}blueprint-url=${blueprintDataUrl}`; + + const joinWithNewline = (segments) => segments.join('\n'); + const defaultButtonImageUrl = 'https://raw.githubusercontent.com/WordPress/wordpress-playground/refs/heads/trunk/.github/workflows/playground-preview-button.svg'; + + const defaultButtonTemplate = joinWithNewline([ + '', + ' Open WordPress Playground Preview', + '' + ]); + + const defaultDescriptionTemplate = joinWithNewline([ + '{{PLAYGROUND_BUTTON}}', + ]); + + const defaultCommentTemplate = joinWithNewline([ + '### WordPress Playground Preview', + '', + 'The changes in this pull request can previewed and tested using a WordPress Playground instance.', + '', + '{{PLAYGROUND_BUTTON}}', + ]); + + const baseTemplateVars = { + PR_NUMBER: String(prNumber), + PR_TITLE: prTitle, + PR_HEAD_REF: headRef, + PR_HEAD_SHA: headSha, + PR_BASE_REF: baseRef, + REPO_OWNER: owner, + REPO_NAME: repoName, + REPO_FULL_NAME: repoFullName, + REPO_ARCHIVE_ROOT: repoArchiveRoot, + REPO_SLUG: repoSlug, + PLUGIN_PATH: pluginPath, + THEME_PATH: themePath, + PLUGIN_SLUG: pluginSlug, + THEME_SLUG: themeSlug, + PLAYGROUND_HOST: playgroundHost + }; + + const templateVariables = mergeVariables( + baseTemplateVars, + { + PLAYGROUND_URL: previewUrl, + PLAYGROUND_BLUEPRINT_JSON: blueprintJson, + PLAYGROUND_BLUEPRINT_DATA_URL: blueprintDataUrl, + PLAYGROUND_BUTTON_IMAGE_URL: defaultButtonImageUrl, + PLAYGROUND_BUTTON: substitute(defaultButtonTemplate, {}) + } + ); + + templateVariables.PLAYGROUND_BUTTON = substitute(defaultButtonTemplate, templateVariables); + + const descriptionTemplate = descriptionTemplateInput && descriptionTemplateInput.trim().length + ? descriptionTemplateInput + : defaultDescriptionTemplate; + const commentTemplate = commentTemplateInput && commentTemplateInput.trim().length + ? commentTemplateInput + : defaultCommentTemplate; + + const renderedDescription = substitute(descriptionTemplate, templateVariables); + const renderedComment = substitute(commentTemplate, templateVariables); + + const performDescriptionUpdate = async () => { + const currentBody = pr.body || ''; + const managedBlock = `${descriptionMarkerStart}${String.fromCodePoint(10)}${renderedDescription.trim()}${String.fromCodePoint(10)}${descriptionMarkerEnd}`; + let nextBody; + + if (currentBody.includes(descriptionMarkerStart) && currentBody.includes(descriptionMarkerEnd)) { + // Markers exist - check if there's a user placeholder + const pattern = new RegExp( + `${descriptionMarkerStart}([\\s\\S]*?)${descriptionMarkerEnd}`, + 'm' + ); + const match = currentBody.match(pattern); + if (match) { + const existingContent = match[1].trim(); + // If content exists but doesn't contain typical button HTML, assume it's a user placeholder + const looksLikeButton = existingContent.includes(' { + const currentBody = pr.body || ''; + if (!currentBody.includes(descriptionMarkerStart) || !currentBody.includes(descriptionMarkerEnd)) { + return; + } + + const pattern = new RegExp( + `${descriptionMarkerStart}[\\s\\S]*?${descriptionMarkerEnd}\\s*`, + 'm' + ); + const nextBody = currentBody.replace(pattern, '').trimEnd(); + + if (nextBody !== currentBody) { + await github.rest.pulls.update({ + owner, + repo: repoName, + pull_number: prNumber, + body: nextBody + }); + core.info('Removed managed Playground block from PR description (comment mode active).'); + } + }; + + const performCommentUpdate = async () => { + const managedBody = `${commentIdentifier}${String.fromCodePoint(10)}${renderedComment.trim()}`; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo: repoName, + issue_number: prNumber, + per_page: 100 + }); + + const existing = comments.find((comment) => + typeof comment.body === 'string' && comment.body.includes(commentIdentifier) + ); + + if (existing) { + if (existing.body !== managedBody) { + await github.rest.issues.updateComment({ + owner, + repo: repoName, + comment_id: existing.id, + body: managedBody + }); + core.info(`Updated existing preview comment (id: ${existing.id}).`); + } else { + core.info('Preview comment already up to date.'); + } + return existing.id; + } + + const created = await github.rest.issues.createComment({ + owner, + repo: repoName, + issue_number: prNumber, + body: managedBody + }); + core.info(`Posted new preview comment (id: ${created.data.id}).`); + return created.data.id; + }; + + let commentId = ''; + if (mode === 'append-to-description') { + await performDescriptionUpdate(); + } else { + await removeManagedDescriptionBlock(); + commentId = String(await performCommentUpdate() || ''); + } + + core.setOutput('mode', mode); + core.setOutput('preview-url', previewUrl); + core.setOutput('blueprint-json', blueprintJson); + core.setOutput('rendered-description', renderedDescription); + core.setOutput('rendered-comment', renderedComment); + core.setOutput('comment-id', commentId); + + outputs: + preview-url: ${{ steps.preview.outputs.preview-url }} + blueprint-json: ${{ steps.preview.outputs.blueprint-json }} + rendered-description: ${{ steps.preview.outputs.rendered-description }} + rendered-comment: ${{ steps.preview.outputs.rendered-comment }} + mode: ${{ steps.preview.outputs.mode }} + comment-id: ${{ steps.preview.outputs.comment-id }} diff --git a/packages/docs/site/docs/main/guides/for-plugin-developers.md b/packages/docs/site/docs/main/guides/for-plugin-developers.md index c6f491a8a1..ed84559877 100644 --- a/packages/docs/site/docs/main/guides/for-plugin-developers.md +++ b/packages/docs/site/docs/main/guides/for-plugin-developers.md @@ -222,3 +222,13 @@ Here's a little demo of this workflow in action: Check [About Playground > Build > Synchronize your playground instance with a local folder and create GitHub Pull Requests](/about/build#synchronize-your-playground-instance-with-a-local-folder-and-create-github-pull-requests) for more info. ::: + +### Add Playground Preview buttons to your Pull Requests + +Make it easier for reviewers and testers to try out your plugin changes by automatically adding a "Preview in WordPress Playground" button to your pull requests. This allows anyone to test your changes instantly without any local setup. + +:::tip + +Check out the [Adding Playground Preview Buttons to Pull Requests](/guides/github-pr-previews) guide to learn how to set up this workflow in your plugin repository. + +::: diff --git a/packages/docs/site/docs/main/guides/for-theme-developers.md b/packages/docs/site/docs/main/guides/for-theme-developers.md index 0683a45c5d..697e564b0b 100644 --- a/packages/docs/site/docs/main/guides/for-theme-developers.md +++ b/packages/docs/site/docs/main/guides/for-theme-developers.md @@ -239,3 +239,13 @@ Note that you'll need the [Create Block Theme](https://wordpress.org/plugins/cre Check [About Playground > Build > Save changes done on a Block Theme and create GitHub Pull Requests](/about/build#save-changes-done-on-a-block-theme-and-create-github-pull-requests) for more info. ::: + +### Add Playground Preview buttons to your Pull Requests + +Make it easier for reviewers and testers to try out your theme changes by automatically adding a "Preview in WordPress Playground" button to your pull requests. This allows anyone to test your theme instantly without any local setup. + +:::tip + +Check out the [Adding Playground Preview Buttons to Pull Requests](/guides/github-pr-previews) guide to learn how to set up this workflow in your theme repository. + +::: diff --git a/packages/docs/site/docs/main/guides/github-pr-previews.md b/packages/docs/site/docs/main/guides/github-pr-previews.md new file mode 100644 index 0000000000..c809d6e8dc --- /dev/null +++ b/packages/docs/site/docs/main/guides/github-pr-previews.md @@ -0,0 +1,362 @@ +--- +title: Adding Playground Preview Buttons to Pull Requests +slug: /guides/github-pr-previews +description: Automatically add a "Preview in WordPress Playground" button to your pull requests, enabling easy testing and feedback for WordPress plugins and themes. +--- + +# Adding Playground Preview Buttons to Pull Requests + +Playground ships a GitHub workflow that automatically adds a "Preview in WordPress Playground" button to your pull requests: + +![Preview Button Example](../../../static/images/preview-button-example.png) + +This helps reviewers and contributors test your WordPress plugins and themes with a single click. + +## Requirements + +- Your repository must be public +- You need to run this on `pull_request` events +- Required permissions: `pull-requests: write` and `contents: read` + +## Quick start + +First, figure out where your plugin or theme lives in your repository. This matters because the workflow needs to know what to install. + +### Step 1: Find your code + +Open your repository and look at the file structure: + +- Repo with a single plugin or theme? + - **Plugin in the root?** If you see a `.php` file with plugin headers right in the root (like `my-plugin.php`), your `plugin-path` is `.` + - **Plugin in a folder?** If your plugin is in something like `plugins/my-plugin/`, your `plugin-path` is `plugins/my-plugin` + - **Theme in the root?** If you see `style.css` with theme headers in the root, your `theme-path` is `.` + - **Theme in a folder?** If your theme is in `themes/my-theme/`, your `theme-path` is `themes/my-theme` +- **More complex setup?** Skip over to the _Advanced: Custom blueprints_ section below. + +### Step 2: Create the workflow file + +Create a new file at `.github/workflows/pr-preview.yml` in your repository. + +**For a plugin:** + +```yaml +name: PR Preview +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + preview: + permissions: + contents: read + pull-requests: write + uses: WordPress/wordpress-playground/.github/workflows/playground-preview-button.yml@workflow-1.0.0 + with: + plugin-path: . # Change this to match your directory structure +``` + +**For a theme:** + +```yaml +name: PR Preview +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + preview: + permissions: + contents: read + pull-requests: write + uses: WordPress/wordpress-playground/.github/workflows/playground-preview-button.yml@workflow-1.0.0 + with: + theme-path: . # Change this to match your directory structure +``` + +**For both:** + +```yaml +name: PR Preview +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + preview: + permissions: + contents: read + pull-requests: write + uses: WordPress/wordpress-playground/.github/workflows/playground-preview-button.yml@workflow-1.0.0 + with: + plugin-path: plugins/my-plugin # Change these to match + theme-path: themes/my-theme # your directory structure +``` + +### Step 3: Update the path + +Change `plugin-path` or `theme-path` to match where your code actually lives. Use what you found in Step 1. + +### Step 4: Commit and push + +Commit this new file to your repository. The next time someone opens a PR, the preview button will appear. + +### Not working? + +Check the Actions tab in your repository. You'll see error messages there if something went wrong. Common issues: + +- Wrong path (double-check where your plugin/theme actually is) +- Missing plugin header or `style.css` +- Repository isn't public + +## How to configure + +Here's a basic config with the available inputs documented inline: + +```yml +name: PR Playground Preview + +on: + pull_request: + types: + - opened # When someone creates a new PR + - synchronize # When new commits are pushed to the PR + - reopened # When a closed PR is reopened + - edited # When PR title or description changes + +jobs: + # This job adds the preview button to your PR + playground-preview: + name: Update Playground Preview + # Wait for the blueprint to be created first + needs: create-blueprint + permissions: + contents: read # Read repository information + pull-requests: write # Update PR descriptions + issues: write # Post/update comments (PRs are issues under the hood) + uses: WordPress/wordpress-playground/.github/workflows/playground-preview.yml@workflow-1.0.0 + with: + # "append-to-description" – add the button to the PR description + # "post-comment" – create a new comment with the preview button + mode: 'append-to-description' + + # Use our custom blueprint with the PR branch + blueprint: ${{ needs.create-blueprint.outputs.blueprint }} + + # # If you just want to install a plugin from a specific path in this repository + # # and you don't really need an entire custom blueprint, you can used simplified + # # configuration. Uncomment this line, remove the create-blueprint job below, + # # and you're good! + # plugin-path: . + + # # Similarly, you can set up a theme fairly easily: + # theme-path: . + + # # You can customize what gets appended to the PR description by uncommenting + # # and changing the description-template below. Available placeholders are + # # listed in the original workflow file at + # # https://github.com/WordPress/wordpress-playground/blob/d64ad78befeffc8a48e9f5be031b7be051add6ff/.github/workflows/playground-preview.yml#L313 + # description-template: | + # ### Test this PR in WordPress Playground + # + # {{PLAYGROUND_BUTTON}} + + # # Similarly, if this workflow is configured to post a comment, you can customize + # # the content of that comment. Available placeholders are listed in the original + # # workflow file at + # # https://github.com/WordPress/wordpress-playground/blob/d64ad78befeffc8a48e9f5be031b7be051add6ff/.github/workflows/playground-preview.yml#L313 + # comment-template: | + # ## Preview Changes in WordPress Playground + # + # {{PLAYGROUND_BUTTON}} + # + # ### Testing Instructions + # 1. Click the button above + # 2. Go to Plugins → Installed Plugins + # 3. Verify `{{PLUGIN_SLUG}}` is active + # 4. Test the new functionality + + # # Are you hosting your own Playground instance? You change the default Playground URL + # # below: + # playground-host: https://playground.wordpress.net +``` + +Both templates support variables using `{{VARIABLE_NAME}}` syntax. Available variables: + +- `{{PLAYGROUND_BUTTON}}` - The actual button HTML +- `{{PLAYGROUND_URL}}` - Direct link to the preview +- `{{PR_NUMBER}}` - Pull request number +- `{{PR_TITLE}}` - Pull request title +- `{{PR_HEAD_REF}}` - Branch name +- `{{PR_HEAD_SHA}}` - Latest commit +- `{{REPO_OWNER}}` - Repository owner +- `{{REPO_NAME}}` - Repository name +- `{{PLUGIN_SLUG}}` - Derived plugin slug +- `{{THEME_SLUG}}` - Derived theme slug + +### Advanced: Custom blueprints + +Blueprints are JSON files that tell Playground exactly what to do. By default, the workflow creates a simple blueprint that installs your plugin or theme. But you can provide your own blueprint for more complex scenarios. + +Common use cases for custom blueprints: + +- Install additional plugins or themes +- Set specific PHP or WordPress versions +- Import test data +- Configure WordPress settings +- Run setup scripts + +Here's how to create a custom blueprint: + +```yaml +name: PR Playground Preview +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + create-blueprint: + name: Create Blueprint + runs-on: ubuntu-latest + outputs: + blueprint: ${{ steps.blueprint.outputs.result }} + steps: + - name: Create Blueprint + id: blueprint + uses: actions/github-script@v7 + with: + script: | + const blueprint = { + preferredVersions: { + php: "8.3", + wp: "6.4" + }, + steps: [ + { + step: "installPlugin", + pluginData: { + resource: "git:directory", + url: `https://github.com/${context.repo.owner}/${context.repo.repo}.git`, + ref: context.payload.pull_request.head.ref, + path: "/" + }, + options: { activate: true } + }, + { + step: "installPlugin", + pluginData: { + resource: "wordpress.org/plugins", + slug: "woocommerce" + } + } + ] + }; + return JSON.stringify(blueprint); + result-encoding: string + + playground-preview: + name: Post Playground Preview Button + needs: create-blueprint + permissions: + contents: read + pull-requests: write + uses: WordPress/wordpress-playground/.github/workflows/playground-preview-button.yml@workflow-1.0.0 + with: + blueprint: ${{ needs.create-blueprint.outputs.blueprint }} + + # This job builds a custom blueprint with dynamic values from the PR + create-blueprint: + name: Create Blueprint + runs-on: ubuntu-latest + outputs: + # Make the blueprint available to the next job + blueprint: ${{ steps.blueprint.outputs.result }} + steps: + - name: Create Blueprint + id: blueprint + uses: actions/github-script@v7 + with: + script: | + // Build a blueprint object with the current PR's information + const blueprint = { + // Send users straight to wp-admin after loading + landingPage: "/wp-admin/", + steps: [ + { + // Install your plugin from the PR branch + step: "installPlugin", + pluginData: { + resource: "git:directory", + // Clone the current repository + url: `https://github.com/${context.repo.owner}/${context.repo.repo}.git`, + + // Clone from the current Pull Request's branch + ref: context.payload.pull_request.head.ref, + + // "/" means the plugin is in the repository root + // Change this to "path/to/my/plugin" if your plugin is in a subdirectory + path: "/" + } + }, + + // Install a non-standard theme for our plugin. + // Themes can also be sourced from your repository and arbitrary URLs, + // see https://wordpress.github.io/wordpress-playground/blueprints/ + { + "step": "installTheme", + "themeData": { + "resource": "wordpress.org/themes", + "slug": "adventurer" + } + } + + // You can add more steps here: + // - Install additional plugins and themes + // - Import starter content + // - Download files + // - Run wp-cli commands + // - ...anything else! + // Learn more at https://wordpress.github.io/wordpress-playground/blueprints/ + ] + }; + // Return the blueprint as a JSON string + return JSON.stringify(blueprint); + result-encoding: string +``` + +This example installs your plugin plus WooCommerce, and uses WordPress 6.4 with PHP 8.3. + +Learn more about blueprints in the [[WordPress Playground documentation](https://wordpress.github.io/wordpress-playground/blueprints/)](https://wordpress.github.io/wordpress-playground/blueprints/). + +## Troubleshooting + +When in trouble, always start with the "Actions" tab in your GitHub repository and check the log from the last time your workflow ran. You will likely fine useful error information in there. Here's a few common problems: + +**The workflow isn't running** + +Check that: + +- Your repository is public +- You're running on `pull_request` events +- You've granted the right permissions in your workflow file + +**The button isn't appearing** + +Check the Actions tab for error messages. Common issues: + +- Missing required inputs (`plugin-path`, `theme-path`, or `blueprint`) +- Invalid blueprint JSON +- Permission errors + +**The preview doesn't load my code** + +Make sure: + +- Your `plugin-path`, `theme-path` points to the right directory +- Your plugin has a valid main plugin file +- Your theme has a valid `style.css` + +## Learn More + +- [Example repository – simple plugin with a preview button configured](https://github.com/adamziel/preview-in-playground-button-plugin-example) +- [Example repository – plugin with a build workflow using the preview button to test the built artifact](https://github.com/adamziel/preview-in-playground-button-built-artifact-example) +- [Blueprint documentation](https://wordpress.github.io/wordpress-playground/blueprints/) diff --git a/packages/docs/site/docs/main/guides/index.md b/packages/docs/site/docs/main/guides/index.md index 1c57835a5d..e3e2fab7ee 100644 --- a/packages/docs/site/docs/main/guides/index.md +++ b/packages/docs/site/docs/main/guides/index.md @@ -9,6 +9,10 @@ sidebar_class_name: navbar-build-item In this section we present a selection of guides that will help you to both work with, and to better understand, a variety of topics related to [WordPress Playground](/). +## [Adding Playground Preview Buttons to Pull Requests](/guides/github-pr-previews) + +Automatically add a "Preview in WordPress Playground" button to your pull requests, enabling reviewers and testers to instantly try out changes without any local setup. + ## [How to ship a real WordPress site in a native iOS app via Playground?](/guides/wordpress-native-ios-app) Check "Blocknotes", the first app to run WordPress natively on iOS via WordPress Playground. It showcases the potential for seamless mobile web integration using WebAssembly and the WordPress block editor.