diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d7f9d48..6bad200 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,14 +5,14 @@ "url": "https://themegrill.com" }, "description": "Internal Claude Code plugins for ThemeGrill engineering", - "version": "1.7.0", + "version": "1.7.1", "plugins": [ { "name": "claudegrill", "source": "./plugins/claudegrill", "displayName": "Claudegrill", "description": "Set a product up, write a fix to house standards, verify it, review a PR, or sweep a whole product before release.", - "version": "1.7.0", + "version": "1.7.1", "author": { "name": "ThemeGrill Engineering" }, diff --git a/.github/workflows/pro-suite.yml b/.github/workflows/pro-suite.yml index 5ae305b..27b5650 100644 --- a/.github/workflows/pro-suite.yml +++ b/.github/workflows/pro-suite.yml @@ -695,6 +695,28 @@ jobs: restore-keys: | qa-deps-${{ runner.os }}- + # boot-wp.mjs's own downloads, separate from the product's — see the long + # comment on the same step in suite.yml for why each path is here and why + # `sites/` is deliberately excluded (it is the extracted, BOOTED WordPress + # tree, reused across boots with the same mount config; caching it in CI + # would let a stale site from a previous PR's run leak into this one, + # which is the opposite of what a disposable pro boot is for). + # + # Keyed on boot-wp.mjs's own hash: this cache's only reason to invalidate + # is the pinned `@wp-playground/cli` version changing, a different + # lifecycle from the pnpm/Playwright cache above. + - name: Cache Playground's own downloads (WordPress core, the CLI's dependencies) + if: steps.scope.outputs.run == 'true' + uses: actions/cache@v4 + with: + path: | + ~/.npm/_cacache + ~/.wordpress-playground + !~/.wordpress-playground/sites + key: qa-playground-${{ runner.os }}-${{ hashFiles('claudegrill/plugins/claudegrill/scripts/boot-wp.mjs') }} + restore-keys: | + qa-playground-${{ runner.os }}- + - name: Look for a suite manifest if: steps.scope.outputs.run == 'true' id: manifest @@ -742,6 +764,14 @@ jobs: TGQA_LICENSE_ALLCOACH_PRO: ${{ secrets.TGQA_LICENSE_KEY }} TGQA_LICENSE_MAGAZINE_BLOCKS_PRO: ${{ secrets.TGQA_LICENSE_KEY }} TGQA_LICENSE_BLOCKART_BLOCKS_PRO: ${{ secrets.TGQA_LICENSE_KEY }} + TGQA_LICENSE_SPACIOUS_PRO: ${{ secrets.TGQA_LICENSE_KEY }} + TGQA_LICENSE_ACCELERATE_PRO: ${{ secrets.TGQA_LICENSE_KEY }} + TGQA_LICENSE_FLASH_PRO: ${{ secrets.TGQA_LICENSE_KEY }} + TGQA_LICENSE_RADIATE_PRO: ${{ secrets.TGQA_LICENSE_KEY }} + TGQA_LICENSE_CENOTE_PRO: ${{ secrets.TGQA_LICENSE_KEY }} + TGQA_LICENSE_HIMALAYAS_PRO: ${{ secrets.TGQA_LICENSE_KEY }} + TGQA_LICENSE_AMPLE_PRO: ${{ secrets.TGQA_LICENSE_KEY }} + TGQA_LICENSE_ESTORE_PRO: ${{ secrets.TGQA_LICENSE_KEY }} run: | set -o pipefail # The `unlicensed` mode boots the SAME pro code with no licence. That diff --git a/.github/workflows/suite.yml b/.github/workflows/suite.yml index 0119d3c..663e5c4 100644 --- a/.github/workflows/suite.yml +++ b/.github/workflows/suite.yml @@ -498,6 +498,43 @@ jobs: restore-keys: | qa-deps-${{ runner.os }}- + # boot-wp.mjs's own downloads, separate from the product's. Two of them: + # + # ~/.npm/_cacache npm's content-addressable cache. Confirmed by + # reading a real install on disk: the pinned + # `@wp-playground/cli` pulls in @php-wasm's PHP + # binaries for every version it bundles, ~300MB, + # none of which is on a fresh runner. + # ~/.wordpress-playground Playground's OWN disk cache for the + # WordPress core it downloads at boot — confirmed + # by reading `@wp-playground/cli`'s source: it + # skips the network fetch outright when + # `.zip` already exists here. + # + # `sites/` under that second path is excluded on purpose. It is NOT a + # download cache — it is the extracted, booted WordPress tree, keyed by + # the mount config and REUSED across boots with the same key (see the + # blueprint-idempotency fix in CLAUDE.md: a second boot of a cached site + # hit "term already exists"). On a developer's machine that persistence + # is the point. In CI it would do the opposite of what a disposable site + # is for: a stale site from a previous run's PR would survive into this + # one. + # + # Keyed on boot-wp.mjs's own hash, not the lockfile above — this cache's + # only reason to invalidate is the pinned CLI version changing, which is + # a different lifecycle from the product's pnpm/Playwright dependencies. + - name: Cache Playground's own downloads (WordPress core, the CLI's dependencies) + if: steps.scope.outputs.run == 'true' + uses: actions/cache@v4 + with: + path: | + ~/.npm/_cacache + ~/.wordpress-playground + !~/.wordpress-playground/sites + key: qa-playground-${{ runner.os }}-${{ hashFiles('claudegrill/plugins/claudegrill/scripts/boot-wp.mjs') }} + restore-keys: | + qa-playground-${{ runner.os }}- + - name: Look for a suite manifest if: steps.scope.outputs.run == 'true' id: manifest diff --git a/CLAUDE.md b/CLAUDE.md index ee7f20d..d7481b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -617,6 +617,53 @@ decision in the suite layer. org billing page) — the per-job "Report elapsed minutes" step already reports to the run summary, but nothing rolls that up across runs or repos. +- **boot-wp.mjs's own downloads were invisible and uncached, on top of + everything the existing pnpm/Playwright cache already covers — found by + reading a real `npx` install on this machine, not by guessing.** The + existing cache step only ever covered the PRODUCT's dependencies. What + `npx --yes @wp-playground/cli@latest` pulls in on a cold runner was never + measured until now: + - `@php-wasm/*`, which the CLI bundles PHP.wasm binaries for every supported + PHP version through, is **~300MB** on disk — measured directly (`du -sh` + across every `.wasm` file under a real `@wp-playground` install), not + estimated. + - Playground has its OWN persistent download cache, `~/.wordpress-playground` + — found by reading `@wp-playground/cli` v3.1.53's source (`We()`: `existsSync(n) + || download(...)`, keyed on `.zip`) and then confirmed on disk: a + real `7.1.zip` (37MB) sitting there from a previous local boot. On a + developer's machine this already makes every boot after the first free; + on a GitHub Actions runner, which starts empty every job, it has never + once been reused. + - `~/.wordpress-playground/sites/` is a THIRD thing living at that same + path and must NOT be cached: it is the extracted, booted WordPress tree + itself, keyed by mount config and reused across boots with the same key — + the exact mechanism behind the blueprint-idempotency bug recorded above + (`wp term create` failing "already exists" on a second boot of a cached + site). Caching it in CI would let a stale site from one PR's run leak into + another's, which is the opposite of what a disposable boot is for. + Confirmed by inspecting real `sites/` entries on disk: full `wp-admin`/ + `wp-includes`/`wp-config.php` trees, not download artefacts. + + Two changes: + - `@wp-playground/cli` is now pinned (`3.1.53`) instead of `@latest`, for the + same reason `@playwright/test` is pinned elsewhere in this repo: `@latest` + means CI silently starts running a different Playground release the day + one ships, and it is also what makes a cache key meaningless — a key + cannot promise it matches what was actually resolved when the resolution + itself floats. + - Both workflows gained a second `actions/cache` step, keyed on + `boot-wp.mjs`'s own hash rather than the product's lockfile (a different + invalidation lifecycle), covering `~/.npm/_cacache` and + `~/.wordpress-playground` with `sites/` excluded via a `!` glob. + + **Not verified**: no live CI run has happened, so the cache-hit path — a + second run reusing what a first run saved — is unproven. What IS verified: + the pinned version resolves and runs via `npx` (confirmed locally), both + workflow files parse, and every path/exclusion above was read from real + source and real directories on disk, not assumed. The `!` exclusion glob + inside a multi-line `path:` is a documented `@actions/glob` pattern but has + not been observed working inside an actual `actions/cache` run in this repo. + - **Three faults in this repo's own cost controls, found by them failing.** Worth recording because each one turned a diagnosable failure into an opaque one: diff --git a/PRO.md b/PRO.md index 2052225..5b5ab70 100644 --- a/PRO.md +++ b/PRO.md @@ -36,6 +36,14 @@ Where a row is incomplete it says so; nothing here is inferred. | **AllCoach Pro** | `themegrill/allcoach-pro` | **ThemeGrill SDK** | `https://api.themegrill.com/licenses/`, item_id `58` | `allcoach_pro_license` | `allcoach_pro_license_status` | `LicenseManager::activate()` — `src/Pro/Core/License/LicenseManager.php:39` → `ThemeGrillSDK\Modules\Licenser::activate()` at `vendor/themegrill/themegrill-sdk/src/Modules/Licenser.php:94` | | **Magazine Blocks Pro** | `themegrill/magazine-blocks-pro` | **Freemius** | id `15516`, slug `magazine-blocks-pro`, `pk_887f5d98b26b4d26ee2253d83ee8e` | Freemius `fs_accounts` | Freemius `fs_accounts` | `magazine_blocks_pro_freemius()` — `magazine-blocks-pro.php:132`, wrapping `fs_dynamic_init()` at `:142` | | **BlockArt Blocks Pro** | `themegrill/blockart-blocks-pro` | **Freemius** | id `15515`, slug `blockart-blocks-pro`, `pk_4acfc44f72d6c0d7bc6abd186ae44` | Freemius `fs_accounts` | Freemius `fs_accounts` | `blockart_pro_freemius()` — `blockart-pro.php:132`, wrapping `fs_dynamic_init()` at `:142` | +| **Spacious Pro** | `themegrill/spacious-pro` | **Freemius** | id `4217`, slug `spacious`, `pk_38fe5c4b7d4937b449e2899a2f8fd` | Freemius `fs_accounts` | Freemius `fs_accounts` | `FS_ThemeGrill::init()` — `functions.php:362`, wrapping `fs_dynamic_init()` at `functions.php:317` | +| **Accelerate Pro** | `themegrill/accelerate-pro` | **Freemius** | id `4215`, slug `accelerate`, `pk_6a52bfb38f6bc816da4643b9ebbdc` | Freemius `fs_accounts` | Freemius `fs_accounts` | `FS_ThemeGrill::init()` — `functions.php:330`, wrapping `fs_dynamic_init()` at `functions.php:285` | +| **Flash Pro** | `themegrill/flash-pro` | **Freemius** | id `4223`, slug `flash`, `pk_9b5963cad22158476546c3b6b9d74` | Freemius `fs_accounts` | Freemius `fs_accounts` | `FS_ThemeGrill::init()` — `functions.php:738`, wrapping `fs_dynamic_init()` at `functions.php:693` | +| **Radiate Pro** | `themegrill/radiate-pro` | **Freemius** | id `4214`, slug `radiate`, `pk_0f1958ed39075ce23559b1c1b48dc` | Freemius `fs_accounts` | Freemius `fs_accounts` | `FS_ThemeGrill::init()` — `functions.php:539`, wrapping `fs_dynamic_init()` at `functions.php:494` | +| **Cenote Pro** | `themegrill/cenote-pro` | **Freemius** | id `4224`, slug `cenote`, `pk_b9adb96523e7353dd99ce5f0beedb` | Freemius `fs_accounts` | Freemius `fs_accounts` | `FS_ThemeGrill::init()` — `functions.php:589`, wrapping `fs_dynamic_init()` at `functions.php:544` | +| **Himalayas Pro** | `themegrill/himalayas-pro` | **Freemius** | id `4210`, slug `himalayas`, `pk_915eca4fa2c0282a4959df24c9207` | Freemius `fs_accounts` | Freemius `fs_accounts` | `FS_ThemeGrill::init()` — `functions.php:243`, wrapping `fs_dynamic_init()` at `functions.php:200` | +| **Ample Pro** | `themegrill/ample-pro` | **Freemius** | id `4213`, slug `ample`, `pk_8cdee14ea6e7fc9263af2ec1a0014` | Freemius `fs_accounts` | Freemius `fs_accounts` | `FS_ThemeGrill::init()` — `functions.php:261`, wrapping `fs_dynamic_init()` at `functions.php:218` | +| **eStore Pro** | `themegrill/estore-pro` | **Freemius** | id `4220`, slug `estore`, `pk_4254cbdc5e2a6d66e77a0525284b9` | Freemius `fs_accounts` | Freemius `fs_accounts` | `FS_ThemeGrill::init()` — `functions.php:558`, wrapping `fs_dynamic_init()` at `functions.php:513` | ### How each product gates a premium feature @@ -50,6 +58,14 @@ This differs per product and is what a `@pro` spec ultimately asserts against. | AllCoach Pro | `get_option('allcoach_pro_license_status') === 'valid'` | | Magazine Blocks Pro | `magazine_blocks_pro_freemius()->can_use_premium_code()` | | BlockArt Blocks Pro | `blockart_pro_freemius()->can_use_premium_code()` | +| Spacious Pro | `FS_ThemeGrill::freemius()->can_use_premium_code()` | +| Accelerate Pro | `FS_ThemeGrill::freemius()->can_use_premium_code()` | +| Flash Pro | `FS_ThemeGrill::freemius()->can_use_premium_code()` | +| Radiate Pro | `FS_ThemeGrill::freemius()->can_use_premium_code()` | +| Cenote Pro | `FS_ThemeGrill::freemius()->can_use_premium_code()` | +| Himalayas Pro | `FS_ThemeGrill::freemius()->can_use_premium_code()` | +| Ample Pro | `FS_ThemeGrill::freemius()->can_use_premium_code()` | +| eStore Pro | `FS_ThemeGrill::freemius()->can_use_premium_code()` | Those last two are a **fourth expression shape** — a plain global function, not the `Class::method` accessor ColorMag and Zakra publish. Both mu-plugins match diff --git a/plugins/claudegrill/licenses.json b/plugins/claudegrill/licenses.json index 82d5528..85a15a9 100644 --- a/plugins/claudegrill/licenses.json +++ b/plugins/claudegrill/licenses.json @@ -141,5 +141,133 @@ "accessor": "new AllCoachPro\\Core\\License\\LicenseManager()", "pro_check": "get_option('allcoach_pro_license_status') === 'valid'", "notes": "Third provider: the ThemeGrill SDK, driven by scripts/lib/license/themegrill-sdk.mjs. Deliberately NOT `edd`: the SDK posts JSON {license_key, item_id, site_url} to store_url+api_path+ (https://api.themegrill.com/licenses/activate) with the action in the PATH, whereas edd.mjs form-posts {edd_action, license, url} to {store}/edd-sl-api/ \u2014 the EDD adapter would produce a well-formed request this store does not understand and a false \"licence not active\". activate/deactivate wrap their payload in a `data` key; `check` returns it flat (Licenser.php:255-260). `option_status` holds a status STRING and the response object goes to `option_data` \u2014 the opposite of user-registration-pro, whose option_status holds the whole object. Licensed iff that string is exactly 'valid': the store also returns license=\"active\" with error=\"site_inactive\" for a good key never activated on this site, and that must NOT open the gate. Replacement plugin sharing the text domain `allcoach` with the free plugin, so the DIRECTORY name identifies it; the two must never be active at once (WordPress does not enforce this). Usage tracking is routed under the base slug `allcoach` because api.themegrill.com/tracking/log rejects `allcoach-pro` with a 400 (src/SDK.php:20-32)." + }, + "spacious-pro": { + "type": "theme", + "repo": "themegrill/spacious-pro", + "requires": "spacious", + "requires_type": "theme", + "delivery": "standalone-theme", + "provider": "freemius", + "freemius_id": "4217", + "freemius_slug": "spacious", + "freemius_public_key": "pk_38fe5c4b7d4937b449e2899a2f8fd", + "key_env": "TGQA_LICENSE_SPACIOUS_PRO", + "entry_point": "FS_ThemeGrill::init() — functions.php:362, wrapping fs_dynamic_init() at functions.php:317", + "accessor": "FS_ThemeGrill::freemius()", + "pro_check": "FS_ThemeGrill::freemius()->can_use_premium_code()", + "notes": "Same shape as colormag-pro: a standalone pro THEME replacing the free spacious, never active alongside it; Freemius slug `spacious` with premium_slug `spacious-pro`; the SDK is a git submodule at freemius/, so checkout needs `submodules: recursive` or FS_ThemeGrill::init() silently returns null. `is_premium_only`, and the only can_use_premium_code() call outside the SDK is inc/freemius-migration.php, so like the block plugins a green @pro run proves the licence RESOLVED, not that any feature is gated behind it." + }, + "accelerate-pro": { + "type": "theme", + "repo": "themegrill/accelerate-pro", + "requires": "accelerate", + "requires_type": "theme", + "delivery": "standalone-theme", + "provider": "freemius", + "freemius_id": "4215", + "freemius_slug": "accelerate", + "freemius_public_key": "pk_6a52bfb38f6bc816da4643b9ebbdc", + "key_env": "TGQA_LICENSE_ACCELERATE_PRO", + "entry_point": "FS_ThemeGrill::init() — functions.php:330, wrapping fs_dynamic_init() at functions.php:285", + "accessor": "FS_ThemeGrill::freemius()", + "pro_check": "FS_ThemeGrill::freemius()->can_use_premium_code()", + "notes": "Same shape as colormag-pro: a standalone pro THEME replacing the free accelerate, never active alongside it; Freemius slug `accelerate` with premium_slug `accelerate-pro`; the SDK is a git submodule at freemius/, so checkout needs `submodules: recursive` or FS_ThemeGrill::init() silently returns null. `is_premium_only`, and the only can_use_premium_code() call outside the SDK is inc/freemius-migration.php, so like the block plugins a green @pro run proves the licence RESOLVED, not that any feature is gated behind it." + }, + "flash-pro": { + "type": "theme", + "repo": "themegrill/flash-pro", + "requires": "flash", + "requires_type": "theme", + "delivery": "standalone-theme", + "provider": "freemius", + "freemius_id": "4223", + "freemius_slug": "flash", + "freemius_public_key": "pk_9b5963cad22158476546c3b6b9d74", + "key_env": "TGQA_LICENSE_FLASH_PRO", + "entry_point": "FS_ThemeGrill::init() — functions.php:738, wrapping fs_dynamic_init() at functions.php:693", + "accessor": "FS_ThemeGrill::freemius()", + "pro_check": "FS_ThemeGrill::freemius()->can_use_premium_code()", + "notes": "Same shape as colormag-pro: a standalone pro THEME replacing the free flash, never active alongside it; Freemius slug `flash` with premium_slug `flash-pro`; the SDK is a git submodule at freemius/, so checkout needs `submodules: recursive` or FS_ThemeGrill::init() silently returns null. `is_premium_only`, and the only can_use_premium_code() call outside the SDK is inc/freemius-migration.php, so like the block plugins a green @pro run proves the licence RESOLVED, not that any feature is gated behind it." + }, + "radiate-pro": { + "type": "theme", + "repo": "themegrill/radiate-pro", + "requires": "radiate", + "requires_type": "theme", + "delivery": "standalone-theme", + "provider": "freemius", + "freemius_id": "4214", + "freemius_slug": "radiate", + "freemius_public_key": "pk_0f1958ed39075ce23559b1c1b48dc", + "key_env": "TGQA_LICENSE_RADIATE_PRO", + "entry_point": "FS_ThemeGrill::init() — functions.php:539, wrapping fs_dynamic_init() at functions.php:494", + "accessor": "FS_ThemeGrill::freemius()", + "pro_check": "FS_ThemeGrill::freemius()->can_use_premium_code()", + "notes": "Same shape as colormag-pro: a standalone pro THEME replacing the free radiate, never active alongside it; Freemius slug `radiate` with premium_slug `radiate-pro`; the SDK is a git submodule at freemius/, so checkout needs `submodules: recursive` or FS_ThemeGrill::init() silently returns null. `is_premium_only`, and the only can_use_premium_code() call outside the SDK is inc/freemius-migration.php, so like the block plugins a green @pro run proves the licence RESOLVED, not that any feature is gated behind it." + }, + "cenote-pro": { + "type": "theme", + "repo": "themegrill/cenote-pro", + "requires": "cenote", + "requires_type": "theme", + "delivery": "standalone-theme", + "provider": "freemius", + "freemius_id": "4224", + "freemius_slug": "cenote", + "freemius_public_key": "pk_b9adb96523e7353dd99ce5f0beedb", + "key_env": "TGQA_LICENSE_CENOTE_PRO", + "entry_point": "FS_ThemeGrill::init() — functions.php:589, wrapping fs_dynamic_init() at functions.php:544", + "accessor": "FS_ThemeGrill::freemius()", + "pro_check": "FS_ThemeGrill::freemius()->can_use_premium_code()", + "notes": "Same shape as colormag-pro: a standalone pro THEME replacing the free cenote, never active alongside it; Freemius slug `cenote` with premium_slug `cenote-pro`; the SDK is a git submodule at freemius/, so checkout needs `submodules: recursive` or FS_ThemeGrill::init() silently returns null. `is_premium_only`, and the only can_use_premium_code() call outside the SDK is inc/freemius-migration.php, so like the block plugins a green @pro run proves the licence RESOLVED, not that any feature is gated behind it." + }, + "himalayas-pro": { + "type": "theme", + "repo": "themegrill/himalayas-pro", + "requires": "himalayas", + "requires_type": "theme", + "delivery": "standalone-theme", + "provider": "freemius", + "freemius_id": "4210", + "freemius_slug": "himalayas", + "freemius_public_key": "pk_915eca4fa2c0282a4959df24c9207", + "key_env": "TGQA_LICENSE_HIMALAYAS_PRO", + "entry_point": "FS_ThemeGrill::init() — functions.php:243, wrapping fs_dynamic_init() at functions.php:200", + "accessor": "FS_ThemeGrill::freemius()", + "pro_check": "FS_ThemeGrill::freemius()->can_use_premium_code()", + "notes": "Same shape as colormag-pro: a standalone pro THEME replacing the free himalayas, never active alongside it; Freemius slug `himalayas` with premium_slug `himalayas-pro`; the SDK is a git submodule at freemius/, so checkout needs `submodules: recursive` or FS_ThemeGrill::init() silently returns null. `is_premium_only`, and the only can_use_premium_code() call outside the SDK is inc/freemius-migration.php, so like the block plugins a green @pro run proves the licence RESOLVED, not that any feature is gated behind it." + }, + "ample-pro": { + "type": "theme", + "repo": "themegrill/ample-pro", + "requires": "ample", + "requires_type": "theme", + "delivery": "standalone-theme", + "provider": "freemius", + "freemius_id": "4213", + "freemius_slug": "ample", + "freemius_public_key": "pk_8cdee14ea6e7fc9263af2ec1a0014", + "key_env": "TGQA_LICENSE_AMPLE_PRO", + "entry_point": "FS_ThemeGrill::init() — functions.php:261, wrapping fs_dynamic_init() at functions.php:218", + "accessor": "FS_ThemeGrill::freemius()", + "pro_check": "FS_ThemeGrill::freemius()->can_use_premium_code()", + "notes": "Same shape as colormag-pro: a standalone pro THEME replacing the free ample, never active alongside it; Freemius slug `ample` with premium_slug `ample-pro`; the SDK is a git submodule at freemius/, so checkout needs `submodules: recursive` or FS_ThemeGrill::init() silently returns null. `is_premium_only`, and the only can_use_premium_code() call outside the SDK is inc/freemius-migration.php, so like the block plugins a green @pro run proves the licence RESOLVED, not that any feature is gated behind it." + }, + "estore-pro": { + "type": "theme", + "repo": "themegrill/estore-pro", + "requires": "estore", + "requires_type": "theme", + "delivery": "standalone-theme", + "provider": "freemius", + "freemius_id": "4220", + "freemius_slug": "estore", + "freemius_public_key": "pk_4254cbdc5e2a6d66e77a0525284b9", + "key_env": "TGQA_LICENSE_ESTORE_PRO", + "entry_point": "FS_ThemeGrill::init() — functions.php:558, wrapping fs_dynamic_init() at functions.php:513", + "accessor": "FS_ThemeGrill::freemius()", + "pro_check": "FS_ThemeGrill::freemius()->can_use_premium_code()", + "notes": "Same shape as colormag-pro: a standalone pro THEME replacing the free estore, never active alongside it; Freemius slug `estore` with premium_slug `estore-pro`; the SDK is a git submodule at freemius/, so checkout needs `submodules: recursive` or FS_ThemeGrill::init() silently returns null. `is_premium_only`, and the only can_use_premium_code() call outside the SDK is inc/freemius-migration.php, so like the block plugins a green @pro run proves the licence RESOLVED, not that any feature is gated behind it." } } diff --git a/plugins/claudegrill/scripts/boot-wp.mjs b/plugins/claudegrill/scripts/boot-wp.mjs index c6f44f6..f975356 100644 --- a/plugins/claudegrill/scripts/boot-wp.mjs +++ b/plugins/claudegrill/scripts/boot-wp.mjs @@ -40,6 +40,27 @@ const qaHome = resolveQaHome(here); const stateFile = path.join(os.tmpdir(), "claudegrill-playground.json"); const logFile = path.join(os.tmpdir(), "claudegrill-playground.log"); +// Pinned, not `@latest`. Two independent reasons, the same lesson this repo +// already paid for once with `@playwright/test` (a browser cache full of the +// wrong revision reads as an empty one — see CLAUDE.md): +// +// 1. `@latest` means CI silently starts running a different Playground +// version the day a new one ships, with no changelog review and no +// diff to point at when something that used to boot stops booting. +// 2. It is what makes caching this download possible at all. `npx --yes +// @latest` re-resolves the dist-tag every invocation, so a runner +// cache keyed on anything stays a guess about whether the resolved +// version actually matches what got cached. Pinned, the version IS the +// cache key (see the workflows' "Cache Playground's own downloads" +// step, keyed on this file's own hash) and a cold runner reuses a warm +// one exactly, every time. +// +// `@php-wasm/*`, which this package bundles PHP.wasm binaries for every +// supported PHP version through, is ~300MB on its own — confirmed by +// inspecting a real `npx` install on disk, not estimated. Bump this +// deliberately, after checking the new version still boots correctly. +const PLAYGROUND_CLI_VERSION = "3.1.53"; + // ------------------------------------------------------------------ arguments const argv = process.argv.slice(2); @@ -561,7 +582,7 @@ if (opt.engine === "playground") { const contentDir = info.type === "theme" ? "themes" : "plugins"; const args = [ "--yes", - "@wp-playground/cli@latest", + `@wp-playground/cli@${PLAYGROUND_CLI_VERSION}`, "start", `--path=${info.root}`, "--no-auto-mount",