diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ff1feb..dc96bc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -222,3 +222,56 @@ jobs: name: bibliography-release path: output/release/borges-bibliography-builder.zip if-no-files-found: error + + # Publish the packaged main build to a rolling `main-preview` pre-release so + # WordPress Playground has a stable, CORS-reachable URL for the current main + # HEAD (git:directory is unavailable in live Playground). Isolated in its own + # job so the write-scoped token is never present during checkout/build/package + # (and never on pull_request or codex/** runs), gated behind the full required + # CI suite so the Main-build badge can't install a build from a run that later + # failed, and reuses the exact artifact those gates validated. + publish-main-preview: + name: Publish rolling main preview + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: + - quality + - phpunit + - playwright-playground + - psalm + - package-release + permissions: + contents: write + + steps: + - name: Download packaged plugin artifact + uses: actions/download-artifact@v6 + with: + name: bibliography-release + path: output/release + + # Guard against a slower older run overwriting `main-preview` with a + # stale ZIP. The tip re-check and the asset write live in ONE step so + # the check happens immediately before the upload (minimal TOCTOU + # window): if `main` has advanced past this commit, skip — the newer + # run publishes. Correct regardless of job-scheduling order, unlike + # job-level concurrency (which can cancel the newer run instead). + - name: Publish rolling main preview + # This job has no checkout, so gh cannot infer the repo from a local + # git remote; GH_REPO gives `gh release`/`gh api` their repo context. + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + tip="$(gh api "repos/${{ github.repository }}/commits/main" --jq .sha)" + if [ "$tip" != "${{ github.sha }}" ]; then + echo "::notice::main advanced to ${tip}; skipping stale main-preview publish for ${{ github.sha }}" + exit 0 + fi + if ! gh release view main-preview >/dev/null 2>&1; then + gh release create main-preview \ + --prerelease \ + --title "Main branch preview (rolling)" \ + --notes "Automated rolling build of the \`main\` branch, refreshed on every push. This is **not** a stable release — for production use install the latest tagged release or the WordPress.org listing." + fi + gh release upload main-preview output/release/borges-bibliography-builder.zip --clobber diff --git a/.github/workflows/demo-links.yml b/.github/workflows/demo-links.yml new file mode 100644 index 0000000..f6be294 --- /dev/null +++ b/.github/workflows/demo-links.yml @@ -0,0 +1,22 @@ +name: Demo Link Monitor + +# Verifies the Playground demo blueprints' install URLs stay reachable and never +# regress to the browser-broken `git:directory` resource. Scheduled (external +# release assets can rot over time) plus manual dispatch. Not run on PRs — the +# structural blueprint contract is already covered by src/blueprint.test.js. + +on: + schedule: + - cron: '17 9 * * 1' # Mondays 09:17 UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + check-demo-links: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Check demo install URLs resolve + run: sh ./scripts/check-demo-links.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1158b..cc289c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add a second WordPress Playground demo that boots the current `main` branch build, alongside the existing released-version demo. CI publishes the freshly built plugin to a rolling `main-preview` pre-release so live Playground has a stable, CORS-reachable URL for main HEAD; the README exposes both as separate Playground badges. +- Add a scheduled Demo Link Monitor (`demo-links.yml` / `npm run test:demo-links`) that verifies each Playground blueprint's install URL stays reachable and guards against reintroducing the hosted-browser-broken `git:directory` resource. + ## [1.4.2] - 2026-06-21 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 3500e21..c05a34d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,7 +95,7 @@ A Gutenberg block plugin that accepts DOI identifiers, PubMed/PMID records, BibT ### Playground -The Playground blueprint at `playground/blueprint.json` configures the GitHub/readme demo. It installs the latest GitHub Release ZIP through the WordPress Playground CORS proxy, installs Block Accessibility Checks from WordPress.org, and requires both `"phpExtensionBundles": ["kitchen-sink"]` and `features.intl` for the CSL formatter. The WordPress.org Preview blueprint at `.wordpress-org/blueprints/blueprint.json` is separate; WordPress.org installs Borges automatically there. The E2E tests (`npm run test:e2e:playground`) run against the GitHub/readme blueprint. +The Playground blueprint at `playground/blueprint.json` configures the GitHub/readme demo (released version). It installs the latest GitHub Release ZIP through the WordPress Playground CORS proxy, installs Block Accessibility Checks from WordPress.org, and requires both `"phpExtensionBundles": ["kitchen-sink"]` and `features.intl` for the CSL formatter. A second blueprint at `playground/blueprint-main.json` powers the README "Main build" badge: it installs the `main-preview` rolling pre-release ZIP that CI's `publish-main-preview` job refreshes on every push to `main` — after the full CI suite passes and only when the commit is still `main`'s tip (live Playground can't use `git:directory`, so a stable release asset is how main HEAD boots). The WordPress.org Preview blueprint at `.wordpress-org/blueprints/blueprint.json` is separate; WordPress.org installs Borges automatically there. The E2E tests (`npm run test:e2e:playground`) run against the GitHub/readme blueprint. ### Release @@ -108,3 +108,4 @@ The Playground blueprint at `playground/blueprint.json` configures the GitHub/re - **release.yml** — tag-triggered GitHub Release and ZIP publication - **wp-deploy.yml** — deploys to WordPress.org SVN on published GitHub release, release dispatch, or manual run - **codeql.yml** — security analysis +- **demo-links.yml** — scheduled/manual check that Playground demo blueprint install URLs stay reachable and never regress to the browser-broken `git:directory` resource diff --git a/README.md b/README.md index e125b03..282a153 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ [](https://github.com/dknauss/Borges/actions/workflows/runtime-matrix.yml) [](https://github.com/dknauss/Borges/actions/workflows/codeql.yml) [](https://codecov.io/gh/dknauss/Borges) -[](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/dknauss/Borges/main/playground/blueprint.json) +[](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/dknauss/Borges/main/playground/blueprint.json) +[](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/dknauss/Borges/main/playground/blueprint-main.json) [](https://wordpress.org/plugins/borges-bibliography-builder/) Borges Bibliography Builder is named after Jorge Luis Borges (1899–1986), the Argentine writer, essayist, poet, and librarian whose work imagined infinite libraries, invented books, and self-referential labyrinths. @@ -22,9 +23,12 @@ Just write out your citations or paste DOIs, PubMed/PMID identifiers, and BibTeX ## Try it in WordPress Playground -Install the public release from [WordPress.org](https://wordpress.org/plugins/borges-bibliography-builder/) or launch a disposable WordPress instance with the plugin preinstalled: [Try the Borges Bibliography Builder in WordPress Playground](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/dknauss/Borges/main/playground/blueprint.json). +Install the public release from [WordPress.org](https://wordpress.org/plugins/borges-bibliography-builder/), or launch a disposable WordPress instance with the plugin preinstalled — no setup, no database, and gone when you close the tab. Two Playground demos are available: -The GitHub-hosted demo Blueprint installs the latest GitHub Release ZIP through the WordPress Playground CORS proxy and explicitly requests PHP `intl` support because editor-time CSL formatting runs through the plugin's local PHP formatter. The WordPress.org Preview blueprint is separate; WordPress.org installs Borges automatically there, and the blueprint only seeds demo content and auxiliary plugin setup. +- **[Try the released version](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/dknauss/Borges/main/playground/blueprint.json)** — installs the latest GitHub Release ZIP (the same build published to WordPress.org) through the WordPress Playground CORS proxy. Use this to try the current stable plugin. +- **[Try the current main build](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/dknauss/Borges/main/playground/blueprint-main.json)** — installs the development build of the `main` branch from the rolling `main-preview` pre-release, which CI refreshes on every push to `main`. Use this to preview unreleased changes ahead of the next release; it is not a stable build. + +Both demo Blueprints explicitly request PHP `intl` support because editor-time CSL formatting runs through the plugin's local PHP formatter. The WordPress.org Preview blueprint is separate; WordPress.org installs Borges automatically there, and the blueprint only seeds demo content and auxiliary plugin setup. ## Screenshots @@ -259,7 +263,8 @@ Multisite runtime smoke coverage is included in CI. SQLite is not currently part - [GitHub releases](https://github.com/dknauss/Borges/releases) - [Release readiness checklist](./docs/release-readiness-checklist.md) - [WordPress.org SVN deploy checklist](./docs/wporg-svn-checklist.md) — maintainer-facing notes -- [Playground blueprint](./playground/blueprint.json) — GitHub demo Blueprint; keep its `features.intl` and `phpExtensionBundles` settings aligned with `.wordpress-org/blueprints/blueprint.json` for WordPress.org previews. +- [Playground blueprint](./playground/blueprint.json) — GitHub demo Blueprint (released version); keep its `features.intl` and `phpExtensionBundles` settings aligned with `.wordpress-org/blueprints/blueprint.json` for WordPress.org previews. +- [Playground main-build blueprint](./playground/blueprint-main.json) — GitHub demo Blueprint that runs the current `main` branch from the rolling `main-preview` pre-release. - [Runtime matrix smoke script](./scripts/runtime-matrix/smoke.sh) - [Brand assets](./.wordpress-org/) @@ -267,13 +272,14 @@ WordPress.org branding assets live in [.wordpress-org](./.wordpress-org/), edita ### Playground Blueprint maintenance -The Playground demo and WordPress.org Preview both rely on the PHP formatter used by the editor REST endpoint. That formatter uses `citeproc-php`, which requires PHP `intl`. Keep both Blueprint files in sync: +The Playground demos and WordPress.org Preview all rely on the PHP formatter used by the editor REST endpoint. That formatter uses `citeproc-php`, which requires PHP `intl`. Keep the Blueprint files in sync: -- `playground/blueprint.json` powers the GitHub README and WordPress.org readme demo link; it installs the latest GitHub Release ZIP through the WordPress Playground CORS proxy so the demo exercises the packaged release artifact without direct GitHub asset CORS failures. +- `playground/blueprint.json` powers the GitHub README (Release badge) and WordPress.org readme demo link; it installs the latest GitHub Release ZIP through the WordPress Playground CORS proxy so the demo exercises the packaged release artifact without direct GitHub asset CORS failures. +- `playground/blueprint-main.json` powers the GitHub README Main-build badge; it installs the `borges-bibliography-builder.zip` asset from the rolling `main-preview` pre-release through the same CORS proxy. CI's `publish-main-preview` job refreshes that pre-release on every push to `main` — after the full CI suite passes, and only when the commit is still `main`'s tip — while the `package-release` job just builds and uploads the artifact it consumes (`git:directory` is unavailable in live Playground, so a stable release asset is the reliable way to boot main HEAD). - `.wordpress-org/blueprints/blueprint.json` deploys to WordPress.org SVN as `assets/blueprints/blueprint.json` for the plugin-directory Preview button. WordPress.org installs the plugin automatically in that preview, so this blueprint does not install Borges itself. -- Both files intentionally declare `phpExtensionBundles: ["kitchen-sink"]` and `features: { "networking": true, "intl": true }`. The bundle form follows WordPress.org Preview documentation; the `features.intl` flag is required by the live browser Playground runtime so formatter requests do not fall back with `bibliography_builder_formatter_extension_missing`. +- All three files intentionally declare `phpExtensionBundles: ["kitchen-sink"]` and `features: { "networking": true, "intl": true }`. The bundle form follows WordPress.org Preview documentation; the `features.intl` flag is required by the live browser Playground runtime so formatter requests do not fall back with `bibliography_builder_formatter_extension_missing`. -Run `npm run test -- --runTestsByPath src/blueprint.test.js` after editing either Blueprint. +Run `npm run test -- --runTestsByPath src/blueprint.test.js` after editing any Blueprint. ### Plugin File Structure diff --git a/SPEC.md b/SPEC.md index 091da28..6bd07ca 100644 --- a/SPEC.md +++ b/SPEC.md @@ -985,7 +985,7 @@ SQLite is not currently part of the GitHub runtime matrix. Future runtime work s `citation-js` remains the best fit for BibTeX parsing and fallback DOI support in the editor. Primary browser DOI resolution uses CrossRef's CSL transform endpoint directly because that path works in WordPress Playground without `doi.org` content-negotiation redirects. Formatting is handled by `citeproc-php` on a local WordPress REST endpoint so the editor bundle does not include citeproc-js and the WordPress.org package can avoid non-GPL-compatible CSL/citeproc-js licensing concerns. The block still saves static plain-text output in post content; PHP formatting is an editor-time service, not a frontend render callback. -Because formatter requests depend on `citeproc-php`, any WordPress Playground demo that exercises editor formatting must load PHP `intl`. Keep both `playground/blueprint.json` and `.wordpress-org/blueprints/blueprint.json` configured with both `phpExtensionBundles: ["kitchen-sink"]` and `features: { "networking": true, "intl": true }`. The bundle form matches WordPress.org Preview guidance, while the explicit `features.intl` flag is required by the live browser Playground runtime to avoid `bibliography_builder_formatter_extension_missing` fallback notices. +Because formatter requests depend on `citeproc-php`, any WordPress Playground demo that exercises editor formatting must load PHP `intl`. There are three Blueprints: `playground/blueprint.json` (README "Release" badge — latest GitHub Release ZIP via the CORS proxy), `playground/blueprint-main.json` (README "Main build" badge — the rolling `main-preview` pre-release ZIP that CI's `publish-main-preview` job refreshes on every push to `main`, after the full CI suite passes and only when the commit is still `main`'s tip), and `.wordpress-org/blueprints/blueprint.json` (WordPress.org Preview button). Keep all three configured with both `phpExtensionBundles: ["kitchen-sink"]` and `features: { "networking": true, "intl": true }`. The bundle form matches WordPress.org Preview guidance, while the explicit `features.intl` flag is required by the live browser Playground runtime to avoid `bibliography_builder_formatter_extension_missing` fallback notices. Live browser Playground cannot use `git:directory` (WordPress/wordpress-playground#3875), which is why the main build ships as a rolling release asset. ### Why the editor uses wrapped `@wordpress/icons` imports diff --git a/package.json b/package.json index f9c1025..e8e5c86 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:e2e": "sh ./scripts/test-smoke-e2e.sh", "test:e2e:playground": "sh ./scripts/test-playground-e2e.sh", "test:e2e:lifecycle": "sh ./scripts/test-lifecycle-e2e.sh", + "test:demo-links": "sh ./scripts/check-demo-links.sh", "test:runtime:local": "sh ./scripts/runtime-matrix/smoke.sh", "test:interop:zotero": "sh ./scripts/test-zotero-interop.sh", "test:a11y": "sh ./scripts/test-a11y.sh", diff --git a/playground/blueprint-main.json b/playground/blueprint-main.json new file mode 100644 index 0000000..2e5c1f3 --- /dev/null +++ b/playground/blueprint-main.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://playground.wordpress.net/blueprint-schema.json", + "meta": { + "title": "Borges Bibliography Builder (main build)", + "description": "Launch WordPress Playground with the current development build of the Borges Bibliography Builder plugin from the main branch.", + "author": "danknauss", + "categories": ["plugin", "bibliography", "block-editor"] + }, + "landingPage": "/wp-admin/post.php?post=1&action=edit", + "preferredVersions": { + "php": "8.3", + "wp": "latest" + }, + "phpExtensionBundles": ["kitchen-sink"], + "features": { + "networking": true, + "intl": true + }, + "login": true, + "steps": [ + { + "step": "installPlugin", + "pluginData": { + "resource": "url", + "url": "https://wordpress-playground-cors-proxy.net/?https://github.com/dknauss/Borges/releases/download/main-preview/borges-bibliography-builder.zip" + }, + "options": { + "activate": true, + "targetFolderName": "borges-bibliography-builder" + }, + "ifAlreadyInstalled": "overwrite" + }, + { + "step": "installPlugin", + "pluginData": { + "resource": "wordpress.org/plugins", + "slug": "block-accessibility-checks" + }, + "options": { + "activate": true + } + }, + { + "step": "runPHP", + "code": "\n
Welcome to the Borges Bibliography Builder development build. This Playground runs the current main branch, not a stable release. Use the Bibliography block below, then paste one or more sample entries into the Paste / Import field (separate entries with a blank line).
10.1145/3368089.3409742\n\n10.1038/s41586-020-2649-2\n\nPMID:26673779\n\n@article{knuth1984literate,\n author = {Knuth, Donald E.},\n title = {Literate Programming},\n journal = {The Computer Journal},\n year = {1984},\n volume = {27},\n number = {2},\n pages = {97--111},\n doi = {10.1093/comjnl/27.2.97}\n}\n\n\n\nTip: In the block sidebar, try changing citation style and export actions (CSL-JSON, BibTeX, RIS).
\n\n\n\nEOT;\n\nwp_update_post(\n\tarray(\n\t\t'ID' => 1,\n\t\t'post_title' => 'Borges Bibliography Playground Starter (main build)',\n\t\t'post_content' => $post_content,\n\t)\n);\n" + } + ] +} diff --git a/scripts/check-demo-links.sh b/scripts/check-demo-links.sh new file mode 100755 index 0000000..a00b569 --- /dev/null +++ b/scripts/check-demo-links.sh @@ -0,0 +1,77 @@ +#!/bin/sh +# Demo Link Monitor +# +# Verifies that every WordPress Playground demo blueprint in this repo installs +# from a live, reachable source — and that none reintroduce the browser-broken +# `git:directory` resource (WordPress/wordpress-playground#3875, which crashes +# hosted-browser installs with "createHash is not a function"). +# +# For each blueprint under playground/ and .wordpress-org/blueprints/: +# - fail if any install step uses `resource: "git:directory"` +# - for every `resource: "url"` step, probe the full install URL (through the +# CORS proxy, exactly as Playground fetches it) and fail if it is unreachable. +# +# Run locally: npm run test:demo-links +# In CI: .github/workflows/demo-links.yml (scheduled + manual) + +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) + +if ! command -v jq >/dev/null 2>&1; then + echo "Demo link check FAILED: jq is required but not installed." >&2 + exit 1 +fi + +fail=0 +checked=0 + +for bp in "$ROOT"/playground/blueprint*.json "$ROOT"/.wordpress-org/blueprints/*.json; do + [ -f "$bp" ] || continue + name=$(basename "$bp") + + # Guard: git:directory is broken in the hosted browser Playground. + if jq -e '.. | objects | select(.resource? == "git:directory")' "$bp" >/dev/null 2>&1; then + echo " BROWSER-BROKEN: $name uses resource \"git:directory\" — see wordpress-playground#3875" + fail=1 + fi + + # Check every url-resource install target is reachable. Fail loudly if the + # blueprint is not valid JSON, rather than silently treating an unparseable + # file as "no URLs to check". + if ! urls=$(jq -r '.. | objects | select(.resource? == "url") | .url' "$bp"); then + echo " ERROR: could not parse $name (invalid JSON)" + fail=1 + continue + fi + [ -n "$urls" ] || continue + + # Iterate without a pipe so $fail survives the loop (POSIX subshell rule). + IFS=' +' + for url in $urls; do + [ -n "$url" ] || continue + checked=$((checked + 1)) + # Probe the FULL install URL the Blueprint uses (through the CORS proxy), + # not just the underlying GitHub asset — so a proxy outage that would break + # the live demo is caught even when the raw asset is still reachable. + printf 'checking %-28s %s\n' "$name" "$url" + # Prefer HEAD; some hosts reject it, so fall back to a 1-byte ranged GET. + if curl -fsSL --retry 2 --max-time 60 -o /dev/null -I "$url" 2>/dev/null; then + : + elif curl -fsSL --retry 2 --max-time 60 -o /dev/null -r 0-0 "$url" 2>/dev/null; then + : + else + echo " DEAD LINK: $url (in $name)" + fail=1 + fi + done + unset IFS +done + +if [ "$fail" -ne 0 ]; then + echo "Demo link check FAILED." >&2 + exit 1 +fi + +echo "Demo link check OK — $checked install URL(s) reachable, no git:directory." diff --git a/src/blueprint.test.js b/src/blueprint.test.js index d1de2a1..df0b296 100644 --- a/src/blueprint.test.js +++ b/src/blueprint.test.js @@ -4,6 +4,7 @@ const path = require('path'); const rootDir = path.resolve(__dirname, '..'); const blueprintFiles = [ 'playground/blueprint.json', + 'playground/blueprint-main.json', '.wordpress-org/blueprints/blueprint.json', ]; @@ -46,4 +47,26 @@ describe('Playground blueprints', () => { }) ); }); + + test('main-build blueprint installs from the rolling main-preview pre-release via Playground CORS proxy', () => { + const blueprint = JSON.parse( + fs.readFileSync( + path.join(rootDir, 'playground/blueprint-main.json'), + 'utf8' + ) + ); + + const installStep = blueprint.steps.find( + (step) => step.step === 'installPlugin' + ); + + expect(installStep).toEqual( + expect.objectContaining({ + pluginData: { + resource: 'url', + url: 'https://wordpress-playground-cors-proxy.net/?https://github.com/dknauss/Borges/releases/download/main-preview/borges-bibliography-builder.zip', + }, + }) + ); + }); });