* ci: pin pnpm store path across runner fleets
Nexu ARC resolved the store under _work while hosted/seed used the home
store, so actions/cache version hashes diverged and every Nexu job missed.
Force $HOME/.pnpm-store and add restore-keys prefix fallback.
* test(e2e): lock stable pnpm store path and restore-keys
Add regression assertions for the non-persistent home store pin and
OS-prefix restore-keys so the cross-fleet cache contract cannot regress
silently.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* ci: seed Linux pnpm cache from cache-maintenance
Pinning the store path alone cannot hit until a main job saves under the
new actions/cache version. Add a single ubuntu-24.04 seed job; the cache
is repo-scoped so every Nexu size reuses it.
* fix(plugin-previews): validate baked preview metadata against shipped plugins
The Home Community gallery renders whatever data/plugin-previews/manifest.json
names, and nothing kept that metadata in sync with reality: #4815 deleted 19
example plugins but left all 19 manifest entries behind, #4040 shipped a card
showing another plugin's imagery, a failed re-bake silently leaves the old
clip serving outdated content, and durationMs recorded the intended walk time
rather than the encoded file (23 entries carried holdMs > real duration, so
the card's idle loop pointed past the end of the clip).
- add scripts/check-plugin-preview-manifest.ts to pnpm guard: every manifest
entry must name a shipped plugin, own its clip keys (<id>/<hash>/... or the
legacy <id>.<hash>....), carry a coherent fingerprint, and never share a
clip with another entry; prune the 19 orphaned entries it caught
- bake validation (BAKE_VERSION 5): reject blank clips (whole-clip luma range
via ffprobe signalstats), record durationMs from the encoded file, clamp
holdMs to it, and write a bake-report.json naming skipped/stale/blank
plugins instead of swallowing the failure
- strict mode (--strict / PREVIEW_STRICT=1) fails the run when a bake left
broken metadata behind; the pre-merge validation workflow now runs strict
while the post-merge/nightly publishers stay lenient
* fix(plugins): make the hyperframes example self-contained
The example loaded cdn.tailwindcss.com (a render-blocking head script the
page never used — every style is inline) plus Google Fonts. Inside the
sandboxed gallery iframe or on a slow network the stalled script showed a
long blank instead of the composition, and the CI bake raced the webfonts.
Drop both: the page now renders from local system font stacks with zero
external requests, so the live preview, the detail iframe, and the baked
clip all show the same content.
* fix(plugin-previews): address review — fail fast on probe errors, widen strict trigger paths
- probeClipMs/clipLumaRange now throw instead of returning null: ffprobe is
required validation infrastructure, and a swallowed probe error silently
disabled exactly the checks this PR adds. A thrown error becomes an
'error …' skip, is recorded in bake-report.json under 'errors', and fails
strict mode — while the routine skips every sweep has (non-html plugins
404ing the preview route) still never trip strict.
- bake-plugin-previews-pr.yml now also triggers on plugins/community/**,
.github/actions/bake-previews/**, and itself, so community-plugin preview
changes and bake-recipe changes cannot merge without the strict pre-merge
validation.
* fix(plugin-previews): bake static pages as a held still instead of skipping forever
The strict pre-merge bake on this PR caught five manifest entries that could
NEVER refresh: example-dating-web, example-html-ppt-zhangzara-8-bit-orbit,
example-frame-logo-outro, od-new-generation, example-orbit-notion. All five
are (near-)static pages — the CDP screencast only delivers frames when
something repaints, so they produce 1-4 frames and the old 'frames < 5' guard
skipped them on every bake, nightly included, leaving their committed entries
permanently stale (their old clips are the 103-210ms degenerate files in the
manifest today).
Encode <5-frame captures as a still instead: hold the last frame for the
idle-loop span (half before the trailing repeated concat entry, half
inherited by it, summing to ~HOLD_MS). Only a zero-frame capture is a real
failure now. The blank-luma check still rejects stills of nothing.
* fix(plugin-previews): never persist a manifest entry without a fingerprint
When the preview fingerprint fetch failed, the loop still rendered and — on
an otherwise successful bake — persisted an entry with hash: null and
un-fingerprinted keys: metadata the content-hash reuse skip can never match
again and the new manifest guard rejects. Treat a missing fingerprint as an
infrastructure error instead: skip the plugin, record it under errors in
bake-report.json (strict mode exits non-zero), and leave any committed entry
untouched for the next sweep.
---------
Co-authored-by: Tuola Ge <gexingli@refly.ai>
* feat(ci): bake pipeline slice 1 — previews-diff guard + single rolling PR
Per specs/change/20260618-plugin-preview-bake-pipeline/spec.md rollout step 1
(the smallest change that stops the bleeding on the stacked bake-PR backlog).
- scripts/plugin-previews-diff.mjs: decide whether a manifest's `previews`
subtree changed, ignoring the per-run `generatedAt` timestamp. node:test
coverage in plugin-previews-diff.test.mjs (the #4261 timestamp-only noise
case, entry change, add/remove, key-order stability).
- bake-plugin-previews.yml: open a review PR only when `previews` actually
changed (was: whole-file `git diff` that fired every run because of
generatedAt), and reuse ONE rolling branch (chore/plugin-previews) /
force-update the open PR in place instead of stacking
chore/plugin-previews-<run_id> per run.
- guard.ts: allowlist the two new CI-only .mjs scripts.
Deferred to later slices (same spec): pre-merge same-repo coupling job,
release-cut full bake, tag-union GC, directory-layered artifact keys.
* feat(ci): bake pipeline slices 2-4 — pre-merge coupling, release full bake, GC, directory keys
Completes the spec (specs/change/20260618-plugin-preview-bake-pipeline) on top
of slice 1, all in one PR (GC ships dry-run so staged ENABLEMENT still holds):
- .github/actions/bake-previews: composite action for the shared render + R2
upload core, so the three bake workflows stop duplicating it.
- bake-plugin-previews.yml (post-merge): refactored onto the composite; role is
now uploader + fork path + nightly backstop (rolling PR unchanged).
- bake-plugin-previews-pr.yml (slice 2): pre-merge bake for SAME-REPO PRs —
renders, uploads, and commits the manifest INTO the author's branch so it
rides with the code change. Loop guard checks the head COMMIT author
(git log -1 %ae of head.sha, fetched via full checkout), not head.user.login,
plus a no-op previews-diff guard. Forks fall through to post-merge.
- bake-plugin-previews-release.yml (slice 3): release-cut full bake committing
the authoritative manifest onto release/**, with paths-ignore + bot-author
loop guards.
- scripts/bake-plugin-previews.mjs (slice 4a): directory-layered, content-
addressed keys <id>/<fingerprint>/preview.mp4 (+ poster.jpg), prefix-relative
in the manifest. Daemon consumer already resolves base+key / path.join(dir,key)
so no consumer change; reused flat entries are left untouched (additive).
- scripts/plugin-previews-gc.mjs (slice 4b) + .github/workflows/
bake-plugin-previews-gc.yml: weekly R2 GC. Protected set = keys referenced by
every tag + live release/** HEAD + main; deletes orphans older than a 90d
grace window. DRY-RUN by default (needs --delete AND GC_ENABLE_DELETE=1).
Pure protected-set/orphan logic covered by node:test.
- guard.ts: allowlist the new CI-only .mjs files.
* fix(ci): capture diff-guard result on its own line so a helper error fails the step
Per review: `if [ "$(node plugin-previews-diff.mjs ...)" != changed ]` swallows
the helper's exit 2 (bad args / unreadable manifest) inside command substitution,
so an error reads as empty string → 'unchanged' branch → the manifest PR/commit
is silently skipped despite a successful bake. Capture into diff_result on its
own line (so `set -e` aborts on a helper error) and `case` on the value, treating
unexpected output as a workflow failure. Applied to all three bake workflows.
* fix(ci): satisfy actionlint — quote gc description colon + route PR/dispatch context through env
- bake-plugin-previews-gc.yml: quote the `delete` input description (the
'(default: ...)' colon broke YAML parsing) and pass dispatch inputs via env
(GC_DELETE/GC_GRACE_DAYS) instead of interpolating into the run body.
- bake-plugin-previews-pr.yml: route head.sha/head.ref through HEAD_SHA/HEAD_REF
env vars to avoid the script-injection lint on untrusted PR context.
* fix(ci): hard-gate the release bake job to release/** branches
workflow_dispatch can fire from any ref; the commit step pushes the authoritative
manifest to the triggering ref with contents:write, so a dispatch on main would
write straight to main and bypass the release back-merge. Add a job-level
`if: startsWith(github.ref, 'refs/heads/release/')` guard.
* fix(ci): GC fails closed on partial protected-ref data
Per review (non-blocking but real once deletion is armed): the GC workflow's
protected-ref fetches ended with '|| true', so a transient fetch failure could
leave the tag/release/main protected set incomplete and, with GC_ENABLE_DELETE=1,
prune clips a live release/main still references. Drop the '|| true' (fail the
job if the protected refs can't be fetched), and add a script-side guard that
refuses to delete when the protected set is empty or origin/main's manifest is
unreadable.
* Consolidate PR validation into ci
* Tune absorbed CI runner tiers
* Fold visual validation into CI Playwright shards
* Run PR shards during manual CI dispatch