Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 239 additions & 0 deletions .github/actions/playground-preview/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
# playground-preview

Writes the text of a pull request comment with links that open the pull request's build in
[WordPress Playground](https://playground.wordpress.net/), a throwaway WordPress in the browser,
and a link to download the zip. While the zip is built the comment says "building". If the build
fails it says "failed", with a link to the run.

The action only writes the text, to a file. It posts nothing. The
[pr-comment](../pr-comment/) action posts the file.

Most repositories do not use this action directly. They call the reusable workflow
[`.github/workflows/playground-preview.yml`](../../workflows/playground-preview.yml), which builds
the zip, runs this action, posts its text with pr-comment and keeps the comment up to date. A caller to copy is in
[`templates/workflows/playground-preview.yml`](../../../templates/workflows/playground-preview.yml):

```yaml
on:
pull_request:
types: [opened, reopened, synchronize]

concurrency:
group: playground-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
playground-preview:
permissions:
contents: read
pull-requests: write
uses: stellarwp/plugin-toolbox/.github/workflows/playground-preview.yml@main
secrets:
s3-bucket: ${{ secrets.ZIP_S3_BUCKET }}
s3-access-key-id: ${{ secrets.ZIP_S3_ACCESS_KEY_ID }}
s3-secret-access-key: ${{ secrets.ZIP_S3_SECRET_ACCESS_KEY }}
s3-region: ${{ secrets.ZIP_S3_REGION }}
s3-endpoint: ${{ secrets.ZIP_S3_ENDPOINT }}
```

## What the repository needs

- A `.puprc` that builds the plugin, as for the release zip. The zip is built by
[stellarwp/github-actions' `zip.yml`](https://github.com/stellarwp/github-actions/blob/main/.github/workflows/zip.yml)
with pup.
- A `pup` Composer script that downloads `pup.phar` when it is missing and then runs it, so one
`composer pup` both installs and runs pup. The zip workflow runs `composer -- pup`. Where the phar
lives is up to the repository: this workflow downloads its own copy of pup to ask for the zip name
(see [The zip link](#the-zip-link)), and never reads the repository's.

- The bucket secrets, passed one by one. `secrets: inherit` only works when the caller and this
repository are in the same organization.
- Nothing else for the default preview. Blueprint files only if you want more than the default.

## The comment

```
🎉 Playground preview ready for commit 15e470c

[Built 01/15 05:43 PM EST]

- 🛝 Try in WordPress Playground
- 🛝 Try in WordPress Playground (With sample data)
- 💾 Download zip

Debug output is on.
With sample data: 100 sample orders in test mode. Opens a few minutes slower.
```

One comment per pull request, replaced on every push. It is found by a hidden first line,
`<!-- playground-preview:<owner>/<repo> -->`. Pull requests from forks get no preview: their runs
cannot see the bucket secrets, and their token cannot write a comment.

The comment is posted by the shared [pr-comment](../pr-comment/) action, the same code every
workflow in this repository uses to find and replace a comment. The workflow passes it the file this
action writes and the same hidden first line.

## Blueprints

A [blueprint](https://wordpress.github.io/wordpress-playground/blueprints/) is a JSON file that
tells Playground what to set up. Each blueprint file is one link in the comment.

With no `blueprints` input, the default blueprint next to this action is used. It works for any
plugin: it installs the zip, activates it, turns on `WP_DEBUG` and `WP_DEBUG_DISPLAY`, logs in and
opens the plugins screen.

Anything specific to a plugin goes in the plugin's own blueprint file, not in a workflow input: the
landing page, the theme, extra plugins, WP-CLI steps, PHP and WordPress versions. Put the files in
the repository, for example in `.github/playground/`, and list them:

```yaml
with:
blueprints: |
.github/playground/blueprint.json
.github/playground/blueprint-sample-data.json
```

Two examples to copy are in [`templates/playground/`](../../../templates/playground/).

The rules:

- `{{ZIP_URL}}` is replaced with the link to the pull request's zip. Every blueprint has to use it,
usually in an `installPlugin` step. A file without it fails the run, because its link would not
show the pull request at all.
- Any other `{{NAME}}` in capitals fails the run, so a typo does not reach Playground. Other text,
such as PHP in a `wp eval` command, is left alone.
- `meta.title` is the link's label: `Try in WordPress Playground (<title>)`. A file without one gets
`Try in WordPress Playground`. With several files, at most one may have no title, and no two may
share one.
- `meta.description` is a short note under the links.
- `meta` and `$schema` are removed before the blueprint goes into the link. Playground does not use
them, and Playground's schema would also ask for `meta.author`. An editor that reads `$schema`
may warn about the missing `author` and `title`; the warning does not matter here.

## The link

The blueprint goes into the link itself, as base64 JSON in the URL hash:
`https://playground.wordpress.net/#eyJzdGVwcyI6...`. Nothing is hosted anywhere, and private
repositories work the same as public ones.

- The hash never leaves the browser, so no server limit on the length of a request applies.
- base64 is plain ASCII, so a Markdown link keeps it as it is, and it is shorter than
percent-encoded JSON. Playground reads the bytes as UTF-8, so text like `Café` survives.
- A blueprint with sample data and an extra plugin makes a link of about 1,300 characters.

The limit that applies first is GitHub's: a comment can hold at most 65,536 characters. When the
links would make the comment longer, the step that writes the text fails and names each link's
length. (pr-comment refuses a longer body too, but only says the number.) Make the largest
blueprint smaller, for example by moving setup into a plugin or a WP-CLI command.

## The zip link

pup names the zip after the version, the commit time and the commit hash, for example
`my-plugin.1.2.0-dev-1793396265-15e470cb.zip`. The shared zip workflow has no outputs, so the
reusable workflow asks pup for the same name again after the build, and adds it to `zip-link-base`
(`https://evnt.is/test-zip?file=` by default, the public link to the packaging bucket).

It asks with the shared [zip-name](../zip-name/) action, which downloads the pup release named by
`pup-version` (`2.0.0` by default) and reads the name from it. **Set `pup-version` to the release
your `composer pup` script downloads.** The zip workflow builds the zip with the repository's own
pup, and a name made by another release could differ from it, which would make the link point at a
zip that does not exist.

To skip the build and use a zip from elsewhere, pass `zip-url`. No secret is needed then. This is
how a repository with its own zip workflow uses this, and how to test it without bucket secrets.

## The check names

A job in a called workflow reports as `<caller job id> / <job name>`. With the template's job id,
the checks are `playground-preview / Playground preview start`, `playground-preview / Zip / ...`,
`playground-preview / Playground preview` and, after a failure, `playground-preview / Playground
preview failed`. Update branch protection if it required the old names.

## Inputs of the workflow

| Input | Default | What it does |
|---|---|---|
| `blueprints` | empty: the default blueprint | Blueprint files, one per line. Each is one link |
| `php-version` | `7.4` | PHP that builds `vendor/`. Use the oldest the plugin supports |
| `i18n` | `no` | `yes` runs `pup i18n` in the build |
| `additional-commands` | empty | Run after `pup build`, before `pup package`. Prefer `.puprc` |
| `zip-url` | empty: build the zip | Use this zip instead of building one |
| `pup-version` | `2.0.0` | The pup release that works out the zip's name. Use the one your `composer pup` script downloads |
| `zip-link-base` | `https://evnt.is/test-zip?file=` | Public link to the bucket. The zip name is added to the end |
| `runs-on` | `ubuntu-latest` | Runner of the job that posts the links |
| `time-zone` | `America/New_York` | Time zone of the badge |

| Secret | Needed | What it does |
|---|---|---|
| `s3-bucket`, `s3-access-key-id`, `s3-secret-access-key`, `s3-region`, `s3-endpoint` | unless `zip-url` is set | Upload the zip. A missing one fails the first job with its name |
| `bot-token` | no | Checks out the repository for the build. Defaults to the workflow's token |
| `jenkins-secret` | no | The shared zip workflow requires it, for Slack messages this workflow never sends |
| `composer-token` | no | Private Composer packages |
| `package-token` | no | Private npm or bun packages |

### Permissions the caller grants

```yaml
permissions:
contents: read # check out the repository
pull-requests: write # the sticky comment
```

A called workflow can only lower these. If the caller grants less than a job asks for, GitHub does
not start the run.

## Inputs of the action

| Input | Required | Default | What it does |
|---|---|---|---|
| `status` | yes | | `building`, `ready` or `failed` |
| `zip-url` | for `ready` | | The zip. Must be https |
| `blueprints` | no | the default blueprint | Blueprint files, read from the checkout |
| `comment-marker` | no | `<!-- playground-preview:<owner>/<repo> -->` | First line of the comment |
| `time-zone` | no | `America/New_York` | Time zone of the badge |
| `sha` | no | the pull request's head commit | The commit the comment names |

| Output | What it is |
|---|---|
| `comment-file` | Path of the file that holds the comment text. Pass it to pr-comment as `body-file` |

In a workflow, the two actions go together:

```yaml
- id: links
uses: stellarwp/plugin-toolbox/.github/actions/playground-preview@main
with:
status: ready
zip-url: https://example.com/my-plugin.zip
- uses: stellarwp/plugin-toolbox/.github/actions/pr-comment@main
with:
marker: '<!-- playground-preview:example-org/my-plugin -->'
body-file: ${{ steps.links.outputs.comment-file }}
```

The action needs no token: it does not call the GitHub API.

## Running it locally

You need Node 20 or newer. From a checkout of this repository, print the comment for any zip and
your blueprint files:

```bash
node .github/actions/playground-preview/playground-preview.js \
--zip-url 'https://evnt.is/test-zip?file=my-plugin.1.2.0-dev-1793396265-15e470cb.zip' \
../my-plugin/.github/playground/blueprint.json \
../my-plugin/.github/playground/blueprint-sample-data.json
```

With no file, the default blueprint is used. Nothing is posted. Open a link from the output to try
the blueprint. A file that breaks a rule above exits 1 with the reason.

## How it is tested

`tests/actions/playground-preview/` loads the script in `node --test`, with the real
`@actions/core`, and asserts the text it writes and the output it sets. Finding and replacing the
comment is tested with pr-comment. It also runs the script from the command line. Run `npm install`
once, then `node --test`.
75 changes: 75 additions & 0 deletions .github/actions/playground-preview/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Builds the text of the "Try in WordPress Playground" comment for a pull request: "building", the
# links when the zip is ready, or "failed". The text starts with a hidden first line, the marker.
#
# It neither builds the zip nor posts the comment. The reusable workflow
# .github/workflows/playground-preview.yml builds the zip, and passes the file this action writes
# (the `comment-file` output) to the pr-comment action, which finds the earlier comment by its marker
# and replaces it. So the action also works with a zip from anywhere, and with any way of posting.
#
# The work is in playground-preview.js, run by actions/github-script, so there is nothing to install
# or build: the file in the repository is the file that runs.
name: Playground preview
description: Writes the pull request comment text with WordPress Playground links for a plugin zip

inputs:
status:
description: '`building`, `ready` or `failed`. Picks the comment to write.'
required: true
zip-url:
description: >-
Public https link to the plugin zip. Required for `ready`: it replaces {{ZIP_URL}} in the
blueprints and is the "Download zip" link.
required: false
default: ''
blueprints:
description: >-
Blueprint files, one path per line, relative to the repository root. Each file becomes one
link, in this order. Empty uses the default blueprint next to this action: install the zip,
activate it, turn on WP_DEBUG and WP_DEBUG_DISPLAY. Read only for `ready`, from the checkout,
so the job has to check out the repository first.
required: false
default: ''
comment-marker:
description: >-
Hidden first line of the comment, used to find it again. Empty uses
`<!-- playground-preview:<owner>/<repo> -->`.
required: false
default: ''
time-zone:
description: 'IANA time zone of the time on the badge, e.g. America/New_York.'
required: false
default: 'America/New_York'
sha:
description: 'The commit the comment names. Only the first 7 characters are shown.'
required: false
default: ${{ github.event.pull_request.head.sha }}

outputs:
comment-file:
description: >-
Path of the file that holds the comment text, under the runner's temp directory. Pass it to
the pr-comment action as `body-file`.
value: ${{ steps.comment.outputs.comment-file }}

runs:
using: 'composite'
steps:
- name: Write the Playground preview comment text
id: comment
uses: actions/github-script@v9
env:
# github-script's require() resolves against the workspace of the repository being built,
# not this action's directory, so the path has to be passed in.
ACTION_PATH: ${{ github.action_path }}
# Read from process.env rather than written into the script, so an input is data and never
# part of the program.
INPUT_STATUS: ${{ inputs.status }}
INPUT_ZIP_URL: ${{ inputs.zip-url }}
INPUT_BLUEPRINTS: ${{ inputs.blueprints }}
INPUT_COMMENT_MARKER: ${{ inputs.comment-marker }}
INPUT_TIME_ZONE: ${{ inputs.time-zone }}
INPUT_SHA: ${{ inputs.sha }}
with:
script: |
const { run } = require(`${process.env.ACTION_PATH}/playground-preview.js`)
await run({ core, env: process.env })
37 changes: 37 additions & 0 deletions .github/actions/playground-preview/blueprints/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"$schema": "https://playground.wordpress.net/blueprint-schema.json",
"meta": {
"description": "Debug output is on."
},
"landingPage": "/wp-admin/plugins.php",
"preferredVersions": {
"php": "8.3",
"wp": "latest"
},
"features": {
"networking": true
},
"steps": [
{
"step": "defineWpConfigConsts",
"consts": {
"WP_DEBUG": true,
"WP_DEBUG_DISPLAY": true
}
},
{
"step": "login",
"username": "admin"
},
{
"step": "installPlugin",
"pluginData": {
"resource": "url",
"url": "{{ZIP_URL}}"
},
"options": {
"activate": true
}
}
]
}
Loading
Loading