From a5cf17ded9245c5fae48fba6020cbf6fa409dd89 Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Thu, 2 Jul 2026 17:24:53 -0600 Subject: [PATCH 1/6] feat(playground): add main-build Playground demo alongside release demo Add a second WordPress Playground badge/link that boots the current main branch. CI's package-release job now publishes the freshly built plugin ZIP to a rolling `main-preview` pre-release on every push to main, giving live Playground a stable, CORS-reachable URL for main HEAD (git:directory is unavailable in the live runtime). The existing badge is relabeled "Release" and continues to install the latest tagged release ZIP. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 21 +++++++++++++++ CHANGELOG.md | 4 +++ CLAUDE.md | 2 +- README.md | 22 +++++++++++----- playground/blueprint-main.json | 48 ++++++++++++++++++++++++++++++++++ src/blueprint.test.js | 23 ++++++++++++++++ 6 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 playground/blueprint-main.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ff1feb..4a9718d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,6 +196,8 @@ jobs: needs: - quality - phpunit + permissions: + contents: write steps: - name: Checkout @@ -222,3 +224,22 @@ jobs: name: bibliography-release path: output/release/borges-bibliography-builder.zip if-no-files-found: error + + # Publish the freshly built main artifact 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). Only runs on push to main, never on pull requests. + - name: Publish rolling main preview + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: softprops/action-gh-release@v3 + with: + tag_name: main-preview + name: Main branch preview (rolling) + prerelease: true + make_latest: false + files: output/release/borges-bibliography-builder.zip + overwrite_files: true + body: | + 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1158b..e49ba91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ 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. + ## [1.4.2] - 2026-06-21 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 3500e21..4d14de9 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 `package-release` job refreshes on every push to `main` (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 diff --git a/README.md b/README.md index 3e763b2..72cc049 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ [![Runtime matrix](https://github.com/dknauss/Borges/actions/workflows/runtime-matrix.yml/badge.svg)](https://github.com/dknauss/Borges/actions/workflows/runtime-matrix.yml) [![CodeQL](https://github.com/dknauss/Borges/actions/workflows/codeql.yml/badge.svg)](https://github.com/dknauss/Borges/actions/workflows/codeql.yml) [![codecov](https://codecov.io/gh/dknauss/Borges/branch/main/graph/badge.svg?token=2MSXL46VTF)](https://codecov.io/gh/dknauss/Borges) -[![WordPress Playground](https://img.shields.io/badge/WordPress%20Playground-Try%20it-3858e9.svg?logo=wordpress&logoColor=white)](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/dknauss/Borges/main/playground/blueprint.json) +[![Playground: Release](https://img.shields.io/badge/Playground-Release-3858e9.svg?logo=wordpress&logoColor=white)](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/dknauss/Borges/main/playground/blueprint.json) +[![Playground: Main build](https://img.shields.io/badge/Playground-Main%20build-8858e9.svg?logo=wordpress&logoColor=white)](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/dknauss/Borges/main/playground/blueprint-main.json) [![WordPress.org](https://img.shields.io/badge/WordPress.org-Install-21759b.svg?logo=wordpress&logoColor=white)](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,7 +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). 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. +Install the public release from [WordPress.org](https://wordpress.org/plugins/borges-bibliography-builder/) or launch a disposable WordPress instance with the plugin preinstalled. Two Playground demos are available: + +- **[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. +- **[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; it is not a stable release. + +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 @@ -257,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/) @@ -265,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 `package-release` job refreshes that pre-release on every push to `main` (`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/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).

\n\n\n\n
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\n

Tip: 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/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', + }, + }) + ); + }); }); From d2fb51c7cca12b42016a225f04ca6f0be8f057b6 Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Thu, 2 Jul 2026 18:15:19 -0600 Subject: [PATCH 2/6] ci(playground): add scheduled Demo Link Monitor for blueprint install URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add scripts/check-demo-links.sh and demo-links.yml (weekly + manual) that verify each Playground demo blueprint's install URL stays reachable and hard-fail if a blueprint reintroduces the hosted-browser-broken `git:directory` resource (WordPress/wordpress-playground#3875). Not run on PRs — the structural blueprint contract is already covered by src/blueprint.test.js. Also exposed as `npm run test:demo-links`. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/demo-links.yml | 22 +++++++++++ CHANGELOG.md | 1 + CLAUDE.md | 1 + package.json | 1 + scripts/check-demo-links.sh | 65 ++++++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+) create mode 100644 .github/workflows/demo-links.yml create mode 100755 scripts/check-demo-links.sh 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 e49ba91..cc289c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 diff --git a/CLAUDE.md b/CLAUDE.md index 4d14de9..dc23044 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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/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/scripts/check-demo-links.sh b/scripts/check-demo-links.sh new file mode 100755 index 0000000..7e3ff4f --- /dev/null +++ b/scripts/check-demo-links.sh @@ -0,0 +1,65 @@ +#!/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, resolve the target (stripping the +# CORS-proxy prefix) and fail if it is not reachable. +# +# Run locally: npm run test:demo-links +# In CI: .github/workflows/demo-links.yml (scheduled + manual) + +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +PROXY_PREFIX="https://wordpress-playground-cors-proxy.net/?" + +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. + urls=$(jq -r '.. | objects | select(.resource? == "url") | .url' "$bp" 2>/dev/null || true) + [ -n "$urls" ] || continue + + # Iterate without a pipe so $fail survives the loop (POSIX subshell rule). + IFS=' +' + for url in $urls; do + [ -n "$url" ] || continue + target=${url#"$PROXY_PREFIX"} + checked=$((checked + 1)) + printf 'checking %-28s %s\n' "$name" "$target" + # 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 "$target" 2>/dev/null; then + : + elif curl -fsSL --retry 2 --max-time 60 -o /dev/null -r 0-0 "$target" 2>/dev/null; then + : + else + echo " DEAD LINK: $target (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." From 17b5f1839b8ec550c43807092ced67da31e5e308 Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Thu, 2 Jul 2026 18:40:04 -0600 Subject: [PATCH 3/6] ci(playground): harden main-preview publish per Codex review Address three P2 findings on PR #66's main-preview publish step: - Move publishing out of `package-release` into an isolated `publish-main-preview` job so the `contents: write` token is never present during checkout/build/package and never on pull_request or codex/** runs (package-release is now read-only). - Gate publishing behind the full required CI suite (needs: quality, phpunit, playwright-playground, psalm, package-release) so the Main-build badge can't install a build from a run that later fails. - Add a `concurrency` guard (cancel-in-progress) so a slower older run can't overwrite `main-preview` with a stale ZIP; the newest run wins. The publish job reuses the artifact package-release already validated instead of rebuilding, and the resulting main-preview asset URL is unchanged. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a9718d..6f8fd42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,8 +196,6 @@ jobs: needs: - quality - phpunit - permissions: - contents: write steps: - name: Checkout @@ -225,12 +223,39 @@ jobs: path: output/release/borges-bibliography-builder.zip if-no-files-found: error - # Publish the freshly built main artifact 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). Only runs on push to main, never on pull requests. + # 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 + # Serialize publishes so a slower older run cannot overwrite `main-preview` + # with a stale ZIP after a newer push has landed; the newest run wins. + concurrency: + group: main-preview-publish + cancel-in-progress: true + + steps: + - name: Download packaged plugin artifact + uses: actions/download-artifact@v6 + with: + name: bibliography-release + path: output/release + - name: Publish rolling main preview - if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: softprops/action-gh-release@v3 with: tag_name: main-preview From 343a63d09efa0cb466f162a9b7e78b359f82e9e5 Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Thu, 2 Jul 2026 19:04:20 -0600 Subject: [PATCH 4/6] ci(playground): address Codex round-2 review of main-preview publish - Replace job-level concurrency with a branch-tip check: the publish step runs only if this commit is still `main`'s tip, so a superseded older run skips instead of overwriting `main-preview` with a stale ZIP. Correct regardless of job-scheduling order (job concurrency could cancel the newer run). - Demo Link Monitor now probes the full CORS-proxy install URL the Blueprint uses, not just the underlying GitHub asset, so a proxy outage that breaks the live demo is caught. - Repoint the README Blueprint-maintenance note at `publish-main-preview` (the job that now refreshes the pre-release), not `package-release`. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 24 +++++++++++++++++++----- README.md | 2 +- scripts/check-demo-links.sh | 17 +++++++++-------- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f8fd42..09449d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -242,11 +242,6 @@ jobs: - package-release permissions: contents: write - # Serialize publishes so a slower older run cannot overwrite `main-preview` - # with a stale ZIP after a newer push has landed; the newest run wins. - concurrency: - group: main-preview-publish - cancel-in-progress: true steps: - name: Download packaged plugin artifact @@ -255,7 +250,26 @@ jobs: name: bibliography-release path: output/release + # Guard against a slower older run overwriting `main-preview` with a + # stale ZIP: publish only if this commit is still the tip of `main`. A + # superseded run detects it is behind and skips; the newest run wins. + # Correct regardless of job-scheduling order — unlike job-level + # concurrency, which (per review) can cancel the newer run instead. + - name: Confirm this commit is still main's tip + id: tip + env: + GH_TOKEN: ${{ github.token }} + run: | + tip="$(gh api "repos/${{ github.repository }}/commits/main" --jq .sha)" + if [ "$tip" = "${{ github.sha }}" ]; then + echo "is_tip=true" >> "$GITHUB_OUTPUT" + else + echo "is_tip=false" >> "$GITHUB_OUTPUT" + echo "::notice::main advanced to ${tip}; skipping stale main-preview publish for ${{ github.sha }}" + fi + - name: Publish rolling main preview + if: steps.tip.outputs.is_tip == 'true' uses: softprops/action-gh-release@v3 with: tag_name: main-preview diff --git a/README.md b/README.md index 172ba8a..282a153 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ WordPress.org branding assets live in [.wordpress-org](./.wordpress-org/), edita 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 (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 `package-release` job refreshes that pre-release on every push to `main` (`git:directory` is unavailable in live Playground, so a stable release asset is the reliable way to boot main HEAD). +- `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. - 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`. diff --git a/scripts/check-demo-links.sh b/scripts/check-demo-links.sh index 7e3ff4f..a0c49b7 100755 --- a/scripts/check-demo-links.sh +++ b/scripts/check-demo-links.sh @@ -8,8 +8,8 @@ # # For each blueprint under playground/ and .wordpress-org/blueprints/: # - fail if any install step uses `resource: "git:directory"` -# - for every `resource: "url"` step, resolve the target (stripping the -# CORS-proxy prefix) and fail if it is not reachable. +# - 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) @@ -17,7 +17,6 @@ set -eu ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -PROXY_PREFIX="https://wordpress-playground-cors-proxy.net/?" fail=0 checked=0 @@ -41,16 +40,18 @@ for bp in "$ROOT"/playground/blueprint*.json "$ROOT"/.wordpress-org/blueprints/* ' for url in $urls; do [ -n "$url" ] || continue - target=${url#"$PROXY_PREFIX"} checked=$((checked + 1)) - printf 'checking %-28s %s\n' "$name" "$target" + # 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 "$target" 2>/dev/null; then + 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 "$target" 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: $target (in $name)" + echo " DEAD LINK: $url (in $name)" fail=1 fi done From e03fc925868cb8d5e92c562dbe89fda92ec18e1b Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Thu, 2 Jul 2026 19:43:07 -0600 Subject: [PATCH 5/6] ci(playground): address Codex round-2 review (TOCTOU, jq robustness, docs) - Collapse the main-preview publish into a single tip-checked `gh release` step so the branch-tip re-check happens immediately before writing the shared asset (minimal TOCTOU window); drop the softprops action. - Demo Link Monitor: add a `jq` preflight and fail on unparseable blueprints instead of silently treating them as "no URLs to check". - Repoint the CLAUDE.md Playground note at `publish-main-preview`. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 40 ++++++++++++++----------------------- CLAUDE.md | 2 +- scripts/check-demo-links.sh | 15 ++++++++++++-- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09449d9..993cc90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -251,34 +251,24 @@ jobs: path: output/release # Guard against a slower older run overwriting `main-preview` with a - # stale ZIP: publish only if this commit is still the tip of `main`. A - # superseded run detects it is behind and skips; the newest run wins. - # Correct regardless of job-scheduling order — unlike job-level - # concurrency, which (per review) can cancel the newer run instead. - - name: Confirm this commit is still main's tip - id: tip + # 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 env: GH_TOKEN: ${{ github.token }} run: | tip="$(gh api "repos/${{ github.repository }}/commits/main" --jq .sha)" - if [ "$tip" = "${{ github.sha }}" ]; then - echo "is_tip=true" >> "$GITHUB_OUTPUT" - else - echo "is_tip=false" >> "$GITHUB_OUTPUT" + if [ "$tip" != "${{ github.sha }}" ]; then echo "::notice::main advanced to ${tip}; skipping stale main-preview publish for ${{ github.sha }}" + exit 0 fi - - - name: Publish rolling main preview - if: steps.tip.outputs.is_tip == 'true' - uses: softprops/action-gh-release@v3 - with: - tag_name: main-preview - name: Main branch preview (rolling) - prerelease: true - make_latest: false - files: output/release/borges-bibliography-builder.zip - overwrite_files: true - body: | - 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. + 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/CLAUDE.md b/CLAUDE.md index dc23044..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 (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 `package-release` job refreshes on every push to `main` (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. +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 diff --git a/scripts/check-demo-links.sh b/scripts/check-demo-links.sh index a0c49b7..a00b569 100755 --- a/scripts/check-demo-links.sh +++ b/scripts/check-demo-links.sh @@ -18,6 +18,11 @@ 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 @@ -31,8 +36,14 @@ for bp in "$ROOT"/playground/blueprint*.json "$ROOT"/.wordpress-org/blueprints/* fail=1 fi - # Check every url-resource install target is reachable. - urls=$(jq -r '.. | objects | select(.resource? == "url") | .url' "$bp" 2>/dev/null || true) + # 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). From 9f3cbbdccd420b58699c06e4866b6a1f07d4a00a Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Thu, 2 Jul 2026 20:04:29 -0600 Subject: [PATCH 6/6] ci(playground): fix gh repo context + sync SPEC.md (Codex round-3) - The publish-main-preview job has no checkout, so `gh release` commands had no repository to infer; set GH_REPO so `gh release`/`gh api` resolve the repo without a workspace git remote (would have failed at runtime). - Update SPEC.md (the authoritative Blueprint spec per AGENTS.md) to the three-Blueprint contract, adding playground/blueprint-main.json. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 3 +++ SPEC.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 993cc90..dc96bc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -257,8 +257,11 @@ jobs: # 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 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