Scheduled SuperSync shard 4/6 failed on a transient Docker Hub timeout
pulling postgres:15-alpine (auth.docker.io request canceled) before any
test ran. Wrap the WebDAV and SuperSync 'docker compose up' steps in
nick-fields/retry (3 attempts) so a single registry blip no longer fails
the job; per-attempt timeout_minutes also bounds the unbounded SuperSync
health-wait loop.
* ci: auto-submit iOS and macOS App Store builds for review
The iOS and Mac App Store workflows previously stopped after uploading the
build to App Store Connect via altool, leaving version creation, "What's New"
and submission as manual steps.
Add fastlane lanes (ios/mac release) that upload the prebuilt .ipa / MAS .pkg
using App Store Connect API key auth (reusing the existing notarization key
secrets), push release notes derived from build/release-notes.md, wait for
processing, submit for review and flag automatic release on approval.
Final version tags submit for review; pre-release tags (RC/beta/alpha) and
manual runs only upload the build. Listing metadata and screenshots remain
curated by hand in App Store Connect.
https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ
* ci: wire iOS and Mac Store workflows to fastlane submit lane
The previous commit added the fastlane lanes but the workflow edits were not
applied. Replace the altool validate/upload steps in the iOS and Mac App Store
workflows with the fastlane submit lane: install fastlane, generate the App
Store "What's New" notes and run `fastlane <platform> release` with App Store
Connect API key auth.
Also extend the Mac workflow's harden-runner egress allowlist with the
rubygems and App Store Connect endpoints used by fastlane.
https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ
* fix(ci): harden Apple App Store auto-submission after review
Address review findings on the iOS/macOS App Store automation:
- Tag gating: submit only when the tag has no "-" (final semver), instead of
denylisting RC/beta/alpha. GitHub Actions contains() is case-sensitive and
the repo's RC tags are mostly lowercase (-rc.N), so the old guard would have
auto-submitted release candidates to production review.
- Fastfile: set skip_metadata so deliver no longer reads back and re-uploads
curated listing fields; push only "What's New" via an inline release_notes
hash. Warn against verbose mode (can dump the API key).
- Gemfile.lock: add arm64-darwin/x86_64-darwin platforms so bundle install
works on the macOS runners.
- Workflows: install deps via pinned ruby/setup-ruby (bundler cache), and
resolve the artifact path with a strict nullglob check (exactly one match)
instead of ls | head.
- release-notes script: tighten emphasis regexes so stray * / _ (globs,
snake_case) survive, anchor footer patterns so legitimate "download" lines
are not dropped, and drop a duplicate mkdir.
- Docs: document the hyphen-based gate, single-use build numbers, automatic
release behavior and inline validation.
https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ
* fix(ci): correct deliver metadata + setup-ruby version (second review pass)
Two bugs introduced by the previous review-fix commit, both confirmed against
upstream source:
- Fastfile: skip_metadata: true makes deliver's upload_metadata return early
(verified in fastlane 2.225.0 deliver/lib/deliver/upload_metadata.rb), so the
"What's New" notes were never uploaded. Revert to metadata_path pointing at a
dir that contains only <locale>/release_notes.txt; load_from_filesystem reads
only that file (next unless File.exist?) with no remote read-back, so other
listing fields stay untouched. Removed the now-unused inline release_notes
helper.
- Workflows: ruby/setup-ruby throws when ruby-version is unset and no
.ruby-version file exists (it does not fall back to system Ruby). Pin
ruby-version: '3.3' in both workflows.
Docs updated to match the corrected metadata approach.
https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ
* fix(ci): remove invalid wait_for_uploaded_build from deliver lanes
wait_for_uploaded_build is a pilot/upload_to_testflight option, not a deliver one. Passing it to upload_to_app_store makes fastlane raise on the unknown key and fail both iOS and macOS release lanes on every run. deliver already waits for the build to finish processing during submit (select_build -> wait_for_build_processing_to_be_complete), so no replacement is needed.
Also pin the ruby/setup-ruby comment to its resolved version (v1.310.0), and slice the App Store release notes by code point so a multi-byte character is never split at the 4000-char cap.
---------
Co-authored-by: Claude <noreply@anthropic.com>
GitHub migrates the macos-latest label to macOS 26 between
2026-06-15 and 2026-07-15. Pin the release, Mac App Store, and
manual macOS build jobs to macos-15 to avoid the image swapping
mid-release; macOS 15 is maintained for ~1 more year. build-ios.yml
already runs on macos-26 and is left unchanged.
* chore(plugins): re-bundle document-mode and document Stage A path
Reverts the unbundling from b0cae69ffe. Stage 0 (gzip + throttle, shipped
in 84625be849) handles size; document-mode remains opt-in per context, so
cross-context conflict risk is bounded. Stage A (keyed plugin-persistence
API, issue #7749) is the documented future path for closing the LWW gap
on different-context concurrent edits — picked up when conflicts are
observed in practice.
Design sketch with multi-reviewed phasing lives in
docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md. Predecessor
plan's "Future work" section now links to it.
* test(plugins): cover document-mode bundled load and PLUGIN_USER_DATA LWW
Follow-ups from the multi-review of 199e816479's re-bundling decision:
- E2E smoke test asserts document-mode appears in plugin management so
a typo in BUNDLED_PLUGIN_PATHS fails loudly.
- Spec exercises PLUGIN_USER_DATA conflict resolution end-to-end, which
previously relied on analogy to REMINDER (same array+null branch) but
was never directly asserted after the migration off 'virtual'.
- Stage A plan risks: stale-editor-view gap surfaced by the review;
PluginHooks.PERSISTED_DATA_UPDATE already exists in the API but is
never dispatched host-side — wiring it is the path to a fix.
- background.ts: comment marks the known gap at the registerHook site.
* docs(plugins): plan for wiring PERSISTED_DATA_CHANGED hook
Designs the host-side wiring for the currently-dead
PluginHooks.PERSISTED_DATA_CHANGED so plugins can react to remote-driven
changes to their persisted data. Multi-reviewed twice; v4 trims scope to
host-only (no plugin adoption in this design) and preserves the
multi-review insights as seeds for the follow-up doc-mode adoption
tracked at issue #7752.
Implementation lands in a separate PR.
* fix(ci): pin wiki-sync second checkout to actions/checkout@v5
v6 stores credentials in a $RUNNER_TEMP file referenced via
includeIf.gitdir matchers; the wiki push intermittently runs without
credentials on github-hosted runners (run 26335467138 failed with
"could not read Username for 'https://github.com'"). v5 uses the
path-independent extraheader, which is stable for this second
checkout pattern.
Leaves the primary Checkout Code on v6.0.2 so Dependabot keeps it
current.
Refs: actions/checkout#2321, #2359
* test: harden e2e failure signals
Fail otherwise-passing E2E tests on browser runtime errors, keep Playwright retries disabled, preserve Docker E2E exit codes, and make plugin/WebDAV setup failures hard failures instead of logged or skipped conditions.
* test: harden provider e2e runners
Make WebDAV and SuperSync runner scripts require provider readiness, preserve cleanup and argument forwarding, and fail manual sync clients on uncaught page errors.
* ci: require providers for scheduled e2e
Set required-provider flags in scheduled WebDAV and SuperSync jobs, and remove the duplicate provider runner scripts while keeping local npm aliases inline.
* test: catch e2e teardown pageerrors and tighten fixture
- closeClient now asserts runtime errors AFTER context.close() so
pageerrors emitted during teardown (Angular destroy hooks, late RxJS
errors) are captured instead of dropped. Matches the pattern in
guardContextCloseWithRuntimeErrorCheck.
- test.fixture.ts isolatedContext now spreads Playwright's merged
contextOptions instead of destructuring 23 fields by hand. Future
option additions propagate automatically; the page fixture uses the
shared attachPageErrorCollector and only fails on pageerror (not
console.error, which is too noisy). Guards against a configured 0
timeout being treated as undefined.
- plugin-loading.spec.ts second test now hard-asserts that the plugin
menu entry reappears after re-enable, matching the first test instead
of silently logging when not visible.
* test(sync): stabilize ImmediateUploadService spec
Two complementary fixes for flaky failures observed under full-suite
random-order runs where the upload pipeline silently never fires:
- Pin navigator.onLine = true in beforeEach (restored in afterEach).
isOnline() inside _canUpload reads navigator.onLine directly. The
keyboard-layout spec replaces the whole navigator and the is-online
spec spies on it; if order or restoration ever drifts, every "should
fire upload" test fails trivially while the "should NOT" tests pass.
- Replace tick(2100) with tick(2000); flush(). The await chain inside
withSession() (provider.isReady, withSession entry, uploadPendingOps,
optional LWW re-upload) requires more microtask drain than tick's
fixed-time window reliably provides under load. flush() drains the
pipeline regardless.
* test(e2e): guard skipOnboarding init script against data: frames
The new page-error collector started failing plugin specs because
addInitScript runs in every frame — including the empty data:text/html
iframe that plugin-index swaps in on destroy — and localStorage access
in a data: URL throws SecurityError. Wrap the four setItem calls in
try/catch so the helper noops in storage-less frames.
GH Action will incorrectly mark discussions without the question-answer
format as answered when that is not possible. The action should be fixed
but restricting to the correct categories will also avoid this problem.
Re-ordered the steps to avoid a discussion that is marked as answered
just before the expiry to be false marked as OUTDATED.
Simplified messages.
Shortened waiting period for answered discussions.
The root lockfile pins app-builder-lib's transitive minimatch via the
`overrides` field. npm 10.9.7 (bundled with Node 22 in setup-node@v6)
flags this as drift and fails `npm ci`, while npm 11 accepts it.
ci.yml's main test job uses `npm i`, which tolerates the drift without
mutating the lockfile on disk.
Plugin-Tests has been red on every PR since 2026-05-08 for this reason.
The inner `npm ci` for plugin-specific deps stays strict.
Fetch the GitHub release body for the tag and publish it as an artifact
plus a job-summary block, so the maintainer can paste it into Partner
Center's "What's new" field during the manual appx upload. Falls back to
a pointer message when the release isn't yet published.
The canonical snapcraft container runs snapcraft as a pebble service whose
working-dir is hard-coded to /project, so docker's -w flag is ignored. The
upload was running in /project (empty) and failing with "'sp.snap' is not a
valid file". Mount $PWD/.tmp/app-builds at /project instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The snapcraft-rocks image (ghcr.io/canonical/snapcraft:8_core24) wraps
its entrypoint in /bin/run-snapcraft.sh which does `exec snapcraft "$@"`.
Passing `snapcraft upload sp.snap --release edge` therefore expanded to
`snapcraft snapcraft upload sp.snap --release edge`, and snapcraft
rejected it with "no such command 'snapcraft'" — failing every retry.
Drop the leading 'snapcraft' word so the wrapper sees only the
subcommand and args. Adds a 4-line WHY comment to prevent re-introduction.
The promote workflow (build-publish-to-snap-on-release.yml) is not
affected — it runs the whole job inside `container:`, so its bare
`snapcraft promote ...` already executes inside the image.
* chore: change Claude doc directives
Ensure docs are not only aimed at end users but also that they aren't
too myopic. DeepWiki can provide limitless detail on any topic.
* chore(ci): switch stalesweeper to a hash reference
* chore(ci): decrease frequency of discussion pruning
* chore(ci): fix "unanswered" logic for stale discussions
Close answered Q&A after 90 days as RESOLVED.
Close everything else after 180 days as OUTDATED.
* chore: clean up issue templates wrt docs
Also, invert the expected-observed order for bug report.
The snap-publish workflows have been failing intermittently with
"unable to contact snap store" coming from `sudo snap install snapcraft
--classic`. Both `samuelmeuli/action-snapcraft` and `canonical/action-publish`
use the same install path internally, so neither is a real alternative.
Switch to Canonical's actively-maintained snapcraft-rocks image
(ghcr.io/canonical/snapcraft:8_core24) which has snapcraft 8 pre-installed
and only needs SNAPCRAFT_STORE_CREDENTIALS — no snapd/snap-store handshake
on the runner. promote/upload subcommands and CLI flags are unchanged.
- build-publish-to-snap-on-release.yml: whole job runs in the container
- build.yml: keeps the existing ubuntu-latest job (npm/playwright/electron-builder
need a normal Ubuntu env), but invokes snapcraft via `docker run` for the
upload step
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apple now requires Xcode 26 / iOS 26 SDK for App Store uploads
(mandatory since 2026-04-28). Switch the iOS release workflow to
the macos-26 runner and explicitly select Xcode 26.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New workflow runs each plugin's `npm test` in a parallel matrix when
its directory or a shared package (plugin-api, vite-plugin) changes
on a PR. Detects per-plugin changes via three-dot `git diff` against
the PR base.
* ci(electron): verify packaged app.asar and smoke-test launch
Adds coverage for the gap that let #7320 ship: CI built electron TS
and ran electron-builder on release, but never exercised the packaged
binary. A stray relative import reaching out of electron/** was
compiled by tsc into src/app/util/*.js, fell outside the files glob
in electron-builder.yaml, and was missing from app.asar — the main
process crashed on launch for every 18.2.7 user.
Two new checks, both running on ubuntu-latest:
1. tools/verify-electron-requires.js extracts app.asar and re-resolves
every relative require() under electron/** against the packaged
tree. Runs on every PR that touches electron/**, electron-builder
config, package.json, or the workflow itself. ~10s after the build.
2. A launch-under-xvfb smoke test starts the packaged binary, waits
30s for a crash, and greps stderr for 'Cannot find module' /
'Uncaught Exception'. Runs on push to master, release tags, and
manual dispatch (not every PR — needs xvfb setup + idle wait).
Exposed as npm run electron:verify-asar for local use after dist.
* ci(electron): harden smoke workflow and verify script from review
Review feedback on the previous commit surfaced a handful of real
defects. Addressed here:
verify-electron-requires.js
- Guard resolved paths against escaping the extracted asar tree.
Without this, a relative require with enough `..` climbs above the
temp dir and Node's resolver hits the host filesystem, masking a
genuinely missing module behind a stray file on the CI runner.
- Walk .cjs and .mjs files too. electron/simple-store.test.cjs ships
via the `electron/**/*` glob and was being skipped.
- Move cleanup out of the path where `process.exit(1)` runs inside a
`try/finally` (synchronous exit does not guarantee `finally`), so
the temp dir is reliably removed.
- Code comment noting that esbuild-bundled preload.js is opaque to
this walker — that coverage comes from the launch smoke test.
electron-smoke.yml
- Broaden pull_request paths filter. The previous filter only matched
electron/**, which is exactly the wrong answer: #7320's root cause
was a file under src/app/util/ that electron/ imported. Now uses a
negative list — docs, translations, android/, ios/, other workflows.
- Explicit Xvfb screen geometry (`-screen 0 1280x720x24`) and
`--disable-dev-shm-usage` on the Electron side. Both known CI
flake preventers.
- Expanded crash-marker regex: SIGSEGV, Segmentation fault,
TypeError:, ReferenceError:, FATAL ERROR, Check failed. Also scan
the log during the liveness loop, not just at the end — catches
main-process exceptions that get logged without killing the
process.
- Move the launch log under .tmp/smoke/ so upload-artifact@v7 reliably
finds it on failure; /tmp paths are fragile across runner images.
- Sweep stray electron worker processes on teardown via pkill.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Follow-up to #7297: the simple-store regression test only ran via
local invocation. Add a test:electron npm script and run it in the
CI Tests job so simple-store fixes stay regression-guarded.
* fix(issue): prevent crash from orphan issueProviderId (#7135)
The Jira image-headers effect in task-detail-panel subscribed to
selectIssueProviderById without an error handler, so a task with an
issueProviderId pointing at a deleted provider (e.g. after sync
convergence where taskIdsToUnlink didn't cover all local tasks)
propagated the selector throw to Zone.js as a crash dialog. Wrap the
inner selector observable in catchError that logs and falls back to
of(null); the downstream jiraCfg?.isEnabled guard handles the fallback.
Also drop IssueLog.log(issueProviderKey, issueProvider) from the
throwing variant of the selector: providers may carry credentials
(host, token, apiKey) and IssueLog history is exportable.
* fix(focus-mode): sync tray countdown with in-app timer during breaks
Tray title was rebuilt from a cached currentFocusSessionTime that only
refreshed when CURRENT_TASK_UPDATED fired. addTimeSpent is gated on an
active current task, so during focus-mode breaks or task-less focus
sessions the cache froze while the in-app timer kept ticking.
Add the tick action to taskChangeElectron$ so the cache refreshes every
second whenever the focus timer is running.
Fixes#7278
* fix(ci): restore GitHub Actions SHA pins undone by 0e9218bd68
Commit 0e9218bd68 silently reverted PR #7212 (github-actions-minor group
bump) along with its stated sync/client-id work. This restores the 15
workflow files to their pre-revert state.
Actions restored to newer pinned SHAs:
- actions/upload-artifact v7.0.0 -> v7.0.1
- step-security/harden-runner v2.16.1 -> v2.17.0
- softprops/action-gh-release v2.6.1 -> v3.0.0
- signpath/github-action-submit-signing-request v2.0 -> v2.1
- anthropics/claude-code-action v1.0.89 -> v1.0.93
- docker/build-push-action v7.0.0 -> v7.1.0
- easingthemes/ssh-deploy v5.1.1 -> v6.0.3
* fix: restore i18n, UI, and docs work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted the following work alongside its stated
sync/client-id changes. Files where later master commits (fec7b25f23, etc.)
already re-applied the reverted work are intentionally left untouched.
Restored:
- #7232 docs/long-term-plans/location-based-reminders.md (513 lines)
- #7199 Romanian i18n phase 3 (ro.json + ro-md.json, ~1168 lines)
- #7049 Polish translation improvements
- #7143 planner component styling (4 scss files)
- #7211 add-task-bar preserve time estimate when typing title
- #7208 task.reducer roll-up estimates for added subtasks
- #6767 focus-mode pomodoro reset button
- #7205 plugin-dev github-issue-provider TOKEN description
- #7231 mobile-bottom-nav FAB fix
- a4fe03272 iOS keyboard accessory bar (global-theme + dialog-fullscreen-markdown)
- 309670db3 ShortSyntaxEffects undefined guard
- 667a7986f Dropbox PKCE auth comment/behavior
* fix(electron): restore electron + e2e work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted the following electron/e2e work.
Files already re-fixed by later master commits are left as-is:
- e2e/tests/sync/supersync-archive-conflict.spec.ts (de33234976 + 191d129ff3)
Restored:
- e8a3e156eb fix(electron): Linux autostart IDB backing-store recovery
(re-adds electron/clear-stale-idb-locks.ts + start-app.ts wiring)
- 5ce78a5b63 fix(electron): macOS Cmd+Q / Dock > Quit hang
(setIsQuiting + before-quit delegate to close-handler)
- 46e0fa2d01 fix(sync): FILE_SYNC_LIST_FILES IPC contract
(electronAPI.d.ts + local-file-sync + preload + ipc-events)
- ea1ef16307 fix(android): session-only SAF permissions on OEM devices
- 8865dc0a50 test(e2e): supersync parallel-worker stampede guard
(SUPERSYNC_SERVER_HEALTHY env-var fallback + goto retry loop)
- af7c7687e2 test(e2e): block WS-triggered downloads in non-WS specs
- 265b44db5d test(e2e): premature waitForURL on daily-summary
(this is literally the fix the bad commit's message claimed to add)
Conflict resolutions:
- e2e/utils/supersync-helpers.ts: kept the refined getDoneTaskElement
checks from d64014d086 (later than c558bcab5e) while restoring the
goto retry loop from 8865dc0a50.
- electron/start-app.ts: unioned imports (setIsQuiting +
clearStaleLevelDbLocks from theirs, fs from ours).
* fix(sync): restore sync-core work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted parts of several sync fixes. Most
sync-core reverts have already been re-addressed differently on master
by later commits (1f5184f6e7, 05cd875dd6, 09f5ced2c9, 7df43358ab,
d9158d6adb, 32dbc95ed9, 8c3b08e016, f89fe1ebc3) — those files are
intentionally left untouched to avoid reverting master's newer work.
This PR restores only the pieces that are genuinely still missing:
- e8a3e156eb fix(electron): IDB backing-store autoreload (in-app piece)
operation-log-hydrator.service.ts + .spec.ts (the electron/clear-
stale-idb-locks.ts piece was restored in the prior commit)
Plus three low-risk documentation/cleanup restorations:
- operation-sync.util.ts — add "Nextcloud" to isFileBasedProvider JSDoc
- dropbox.ts — restore improved _getRedirectUri JSDoc (667a7986fb)
- dialog-get-and-enter-auth-code.component.ts — restore isNativePlatform
comment explaining why manual code entry flow is used (667a7986fb)
- file-adapter.interface.ts — remove stray "// NEW" comment (46e0fa2d01)
Intentionally NOT restored (master's newer work covers or supersedes):
- sync-trigger.service.ts / sync.effects.ts (05cd875dd6)
- sync-wrapper.service.ts (1f5184f6e7)
- sync-errors.ts (1f5184f6e7 re-added LegacySyncFormatDetectedError)
- file-based-sync-adapter.service.ts + spec (1f5184f6e7 + d9158d6adb)
- file-based-sync.types.ts (1f5184f6e7)
- operation-log.const.ts (32dbc95ed9 bumped IDB_OPEN_RETRIES to 5)
- dialog-sync-initial-cfg.component.ts (f89fe1ebc3)
- Separate JsonParseError and SyncDataCorruptedError handlers in sync wrapper
- Handle corrupted remote data gracefully instead of throwing
- Add getOrGenerateClientId() as unified entry point, eliminating dual injection
of ClientIdService + CLIENT_ID_PROVIDER in snapshot-upload, file-based-encryption,
and sync-hydration services
- Use crypto.getRandomValues() instead of Math.random() for client ID generation
- Warn user when stored client ID is invalid and must be regenerated
- Fix flaky e2e supersync tests (premature waitForURL match on daily-summary URL)
Despite "failing" in the GH Action logs there is still useful work being done by this action as-is. I have increased the frequency from weekly to daily to accelerate the catch-up process after which I expect it to remain within all rate limits.
I've also re-released by action using SemVer so dependabot should pick up any improvements I make to the rate limit problem itself.
Replace the accent-colored block + checkmark icon on swipe-right with
a dynamic strikethrough line that follows the touch position. The line
extends from left to right tracking the finger, transitions at the
trigger threshold, and snaps to full width on completion.
For undo (swiping on done tasks), the native text-decoration
line-through fades out via text-decoration-color animation, task
opacity increases from dimmed to full, and the checkmark unchecks.
Key implementation details:
- Strikethrough Y position adapts to task title center, with
even-line-count offset for multi-line titles
- Undo uses text-decoration-color animation on .display-value
- showUndoneAnimation input on done-toggle removes is-done class
- Left swipe (context menu) behavior is unchanged
- Zero performance impact on desktop (IS_TOUCH_PRIMARY guard)
Add a new supersync-server-tests job to the scheduled E2E workflow that
runs the SuperSync server unit tests (vitest). These were previously
missing from CI entirely.
Also extend the push path filter to include packages/super-sync-server
and packages/shared-schema so the workflow triggers on pushes to master
that change server code.
Signing failure previously resulted in no exe files in the release
because all publish steps were gated on signing success, but
continue-on-error silently swallowed the failure. Now the job
fails hard if signing fails, making the problem visible. Also
increases SignPath timeout from 10 to 60 minutes.
Root cause: electron-builder auto-published unsigned files via implicit
tag-based publishing (despite release:false), and spaces in NSIS
artifact names caused naming mismatches — electron-builder sanitized
spaces to hyphens while softprops/action-gh-release converted them
to dots, resulting in three sets of exe files per architecture.
Changes:
- Hardcode NSIS artifactName with hyphens to eliminate spaces
- Add --publish never to explicitly prevent auto-publishing
- Update latest.yml with signed file hashes (preserving EB fields)
- Use lockfile-pinned app-builder-bin for blockmap regeneration
- Scope blockmap publishing to NSIS setup files only
- Fail hard if latest.yml references missing files
PowerShell (default on Windows) treats $GITHUB_OUTPUT as an undefined
variable instead of the environment variable, so the cache path output
was never written, causing actions/cache to fail with "path required".
Split PR preview into two workflows using the workflow_run pattern:
- pr-preview-build.yml (pull_request): builds without secret access
- pr-preview-deploy.yml (workflow_run): deploys pre-built artifact with secrets
Prevents fork PR authors from executing arbitrary code with access to
CLOUDFLARE_API_TOKEN, UNSPLASH_KEY, and other repository secrets.