feat: add beta/stable release tracks with switchable update channel (#2345)
## Summary Fusion can now ship on two release tracks. Betas are cut from `main` as `vX.Y.Z-beta.N` (npm dist-tag `beta`, GitHub prerelease), stable releases are promoted to a long-lived `release` branch and published to `latest`, and users pick their track with the new `updateChannel` global setting — via **Settings → General → Release channel** or `fn update --channel <stable|beta>`. Previously everything was single-track: every publish landed on `latest` and every update surface could only see it. | | beta | stable | |---|---|---| | Cut from | `main` | `release` branch | | Version | `X.Y.Z-beta.N` (changesets pre-mode) | `X.Y.Z` | | npm dist-tag | `beta` | `latest` | | GitHub Release | prerelease | latest | | Homebrew tap / X draft | skipped | bumped / printed | ## How releasing works now `pnpm release` prompts for the channel and **defaults to beta**, so day-to-day releases are betas; stable is always an explicit choice. Choosing stable from `main` triggers assisted promotion: the script proposes the newest beta tag reachable from HEAD, verifies `release` fast-forwards to it, then runs the whole stable release inside a temporary git worktree on `release` — the primary checkout never leaves `main`. Changesets pre-mode preserves changeset files across betas, so the promoted stable release aggregates every changeset since the last stable into one clean changelog entry. ## Design decisions - **Every publish path names an explicit `--tag`.** A beta accidentally landing on `latest` is the one unrecoverable failure of a dual-track scheme, so nothing relies on npm's implicit default (`release.mjs`, `version.yml`). - **Beta channel resolves to semver-max of `latest` and `beta`**, so beta users are offered each promoted stable once it overtakes their prerelease. Switching beta → stable never downgrades; `fn update --channel stable --force` is the explicit escape hatch. - **One comparator instead of three.** CLI, dashboard, and desktop each had their own `isRemoteNewer` that ignored prerelease identifiers — `0.73.0-beta.2`, `-beta.3`, and `0.73.0` all compared equal, which breaks the moment any beta exists. They now share full SemVer-precedence helpers (`compareVersions`, `resolveUpdateTargetVersion`) from `@fusion/core`. - **Installs pin exact versions** (`@runfusion/fusion@0.73.0-beta.2`), never a dist-tag, so an install can't silently land on the wrong track. - **Desktop channels via electron-updater manifests.** Beta tags build desktop artifacts with `publish.channel=beta` (emitting `beta*.yml`); the app sets `channel`/`allowPrerelease` from the shared setting, re-read on every manual check. - **Update caches are channel-stamped** — a cache written for one channel is never served to the other, so switching tracks takes effect on the next check instead of after TTL. ## Test plan - New unit coverage: SemVer precedence + channel resolution in `@fusion/core` (30), channel behavior of the dashboard update check (28, incl. 9 new) and `fn update` (16, incl. 8 new: persist `--channel`, no-downgrade, `--force`, cache channel mismatch). - `pnpm verify:fast` green (scoped typecheck, builds, CLI build, boot smoke); desktop + settings-section suites green. - `release.mjs` dry-run matrix exercised by hand: channel prompt (default/override/invalid), branch preflights per channel, assisted-promotion target selection, fast-forward guard against a diverged `release` branch, and bootstrap when no `release` branch exists. - Not exercised live: an end-to-end publish (needs TTY authorization + real npm publish). First real run is the first `pnpm release --channel beta`. --- [](https://github.com/EveryInc/compound-engineering-plugin)  <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added beta and stable release channels across CLI, dashboard, and desktop updates. * Users can select a channel via Settings or `fn update --channel <stable|beta>` (stored as a global default). * Desktop beta releases now generate beta update manifests and publish as prereleases. * **Documentation** * Expanded release-track, settings, and CLI references to explain channel semantics and workflows. * **Bug Fixes** * Updates now pin the resolved version per channel, improve version comparison, and prevent unintended cross-channel downgrades unless `--force` is used. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
25
.github/workflows/release.yml
vendored
25
.github/workflows/release.yml
vendored
@@ -194,7 +194,10 @@ jobs:
|
||||
# Use `exec electron-builder` rather than the `dist:win` script: pnpm leaks
|
||||
# the `--` separator into script args (electron-builder then stops parsing
|
||||
# at `--` and ignores `--publish never`, auto-publishing to the wrong repo).
|
||||
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --win --publish never
|
||||
# FNXC:UpdateChannels 2026-07-19-13:30: beta tags (v*-beta.N) build with
|
||||
# publish.channel=beta so electron-builder emits beta*.yml update manifests;
|
||||
# beta-channel desktop installs read those, stable installs keep latest*.yml.
|
||||
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --win --publish never ${{ contains(github.ref_name, '-beta') && '-c.publish.channel=beta' || '' }}
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -226,6 +229,7 @@ jobs:
|
||||
packages/desktop/dist-electron/Fusion-*-win-*.exe.sha256
|
||||
packages/desktop/dist-electron/Fusion-*-win-*.exe.blockmap
|
||||
packages/desktop/dist-electron/latest.yml
|
||||
packages/desktop/dist-electron/beta.yml
|
||||
|
||||
# ── Build macOS desktop artifacts ────────────────────────────────────
|
||||
build-desktop-macos:
|
||||
@@ -252,7 +256,8 @@ jobs:
|
||||
|
||||
- name: Package signed macOS desktop DMG/ZIP
|
||||
if: ${{ env.APPLE_CERTIFICATE_BASE64 != '' }}
|
||||
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --mac --publish never
|
||||
# FNXC:UpdateChannels 2026-07-19-13:30: beta tags emit beta*.yml manifests (see Windows leg).
|
||||
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --mac --publish never ${{ contains(github.ref_name, '-beta') && '-c.publish.channel=beta' || '' }}
|
||||
env:
|
||||
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
|
||||
@@ -265,7 +270,7 @@ jobs:
|
||||
|
||||
- name: Package unsigned macOS desktop DMG/ZIP
|
||||
if: ${{ env.APPLE_CERTIFICATE_BASE64 == '' }}
|
||||
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --mac --publish never -c.mac.notarize=false
|
||||
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --mac --publish never -c.mac.notarize=false ${{ contains(github.ref_name, '-beta') && '-c.publish.channel=beta' || '' }}
|
||||
env:
|
||||
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
@@ -330,6 +335,7 @@ jobs:
|
||||
packages/desktop/dist-electron/Fusion-*-mac-*.zip.sha256
|
||||
packages/desktop/dist-electron/Fusion-*-mac-*.blockmap
|
||||
packages/desktop/dist-electron/latest-mac.yml
|
||||
packages/desktop/dist-electron/beta-mac.yml
|
||||
|
||||
# ── Build Linux desktop artifacts ────────────────────────────────────
|
||||
build-desktop-linux:
|
||||
@@ -356,7 +362,8 @@ jobs:
|
||||
|
||||
- name: Package Linux desktop artifacts
|
||||
# Linux desktop code-signing is deferred to FN-5605; Linux ARM64 CLI binaries are tracked in FN-5606.
|
||||
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --linux --x64 --arm64 --publish never
|
||||
# FNXC:UpdateChannels 2026-07-19-13:30: beta tags emit beta*.yml manifests (see Windows leg).
|
||||
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --linux --x64 --arm64 --publish never ${{ contains(github.ref_name, '-beta') && '-c.publish.channel=beta' || '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -425,6 +432,7 @@ jobs:
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz.sha256
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz.asc
|
||||
packages/desktop/dist-electron/latest-linux.yml
|
||||
packages/desktop/dist-electron/beta-linux.yml
|
||||
|
||||
|
||||
# ── Build Android APK/AAB artifacts ──────────────────────────────────
|
||||
@@ -619,7 +627,7 @@ jobs:
|
||||
# The self-contained fn-cli-<platform>.tar.gz (+ .sha256) matches the
|
||||
# existing *.tar.gz globs and reaches the release alongside the bare
|
||||
# fn-cli-* binaries.
|
||||
find artifacts \( -path "*/runtime/*" -o -path "*/migrations/*" \) -prune -o -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.asc" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.apk" -o -name "*.aab" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" -o -name "latest*.yml" \) -print -exec cp {} release-files/ \;
|
||||
find artifacts \( -path "*/runtime/*" -o -path "*/migrations/*" \) -prune -o -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.asc" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.apk" -o -name "*.aab" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" -o -name "latest*.yml" -o -name "beta*.yml" \) -print -exec cp {} release-files/ \;
|
||||
ls -la release-files/
|
||||
count=$(find release-files -type f | wc -l | tr -d ' ')
|
||||
echo "count=$count" >> "$GITHUB_OUTPUT"
|
||||
@@ -660,10 +668,17 @@ jobs:
|
||||
")
|
||||
echo "$NOTES" > /tmp/release-notes.md
|
||||
|
||||
# FNXC:UpdateChannels 2026-07-19-13:30:
|
||||
# Beta tags (vX.Y.Z-beta.N, cut from main by `pnpm release --channel beta`)
|
||||
# must be GitHub PRERELEASES: the desktop stable auto-updater and the
|
||||
# /releases/latest URL follow the "latest" release, which GitHub only
|
||||
# assigns to non-prerelease releases. The tag name is the single source
|
||||
# of truth so tag-push-triggered binary builds do the right thing.
|
||||
- name: Create GitHub Release
|
||||
if: ${{ steps.collect.outputs.count != '0' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: /tmp/release-notes.md
|
||||
fail_on_unmatched_files: true
|
||||
prerelease: ${{ contains(github.ref_name, '-beta') }}
|
||||
files: release-files/*
|
||||
|
||||
12
.github/workflows/version.yml
vendored
12
.github/workflows/version.yml
vendored
@@ -2,6 +2,15 @@
|
||||
#
|
||||
# Uses npm OIDC trusted publishing — no NPM_TOKEN secret needed.
|
||||
# Requires npm 11.5.1+ for OIDC support.
|
||||
#
|
||||
# FNXC:UpdateChannels 2026-07-19-13:30:
|
||||
# STABLE-CHANNEL ONLY. This workflow publishes with npm's implicit `latest`
|
||||
# dist-tag and must never run for a beta: betas are cut from `main` by
|
||||
# `pnpm release --channel beta`, which publishes with an explicit `--tag beta`
|
||||
# (see scripts/release.mjs and docs/plans/2026-07-19-001-beta-stable-release-tracks-plan.md).
|
||||
# If beta publishing ever moves to CI, this workflow needs a channel input that
|
||||
# threads `--tag beta` into the publish command — do not dispatch it as-is from
|
||||
# a pre-mode (.changeset/pre.json) checkout.
|
||||
|
||||
name: Version & Release
|
||||
|
||||
@@ -42,7 +51,8 @@ jobs:
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm release:version
|
||||
publish: pnpm -r publish --provenance --access public
|
||||
# Explicit --tag latest: every publish path names its dist-tag (see channel note above).
|
||||
publish: pnpm -r publish --provenance --access public --tag latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
Reference in New Issue
Block a user