## 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>
86 lines
2.5 KiB
TypeScript
86 lines
2.5 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { GlobalSettingsStore, resolveGlobalDir } from "@fusion/core";
|
|
import type { UpdateChannel } from "@fusion/core";
|
|
|
|
type CachedUpdateStatus = {
|
|
updateAvailable: boolean;
|
|
latestVersion: string;
|
|
currentVersion: string;
|
|
channel?: UpdateChannel;
|
|
};
|
|
|
|
type UpdateCachePayload = {
|
|
updateAvailable?: unknown;
|
|
latestVersion?: unknown;
|
|
currentVersion?: unknown;
|
|
channel?: unknown;
|
|
};
|
|
|
|
export function getCachedUpdateStatus(currentVersion?: string): CachedUpdateStatus | null {
|
|
try {
|
|
const cachePath = join(resolveGlobalDir(), "update-check.json");
|
|
const raw = readFileSync(cachePath, "utf-8");
|
|
const parsed = JSON.parse(raw) as UpdateCachePayload;
|
|
|
|
if (
|
|
parsed.updateAvailable === true &&
|
|
typeof parsed.latestVersion === "string" &&
|
|
parsed.latestVersion.length > 0 &&
|
|
typeof parsed.currentVersion === "string" &&
|
|
parsed.currentVersion.length > 0
|
|
) {
|
|
if (
|
|
typeof currentVersion === "string" &&
|
|
currentVersion.length > 0 &&
|
|
parsed.currentVersion !== currentVersion
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
updateAvailable: true,
|
|
latestVersion: parsed.latestVersion,
|
|
currentVersion: parsed.currentVersion,
|
|
channel: parsed.channel === "beta" ? "beta" : parsed.channel === "stable" ? "stable" : undefined,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function isUpdateCheckEnabled(): Promise<boolean> {
|
|
const store = new GlobalSettingsStore();
|
|
await store.init();
|
|
const settings = await store.getSettings();
|
|
return settings.updateCheckEnabled !== false;
|
|
}
|
|
|
|
/*
|
|
FNXC:UpdateChannels 2026-07-19-13:00:
|
|
The persisted `updateChannel` global setting selects the release track for every
|
|
update surface. `fn update --channel <stable|beta>` both uses AND persists the
|
|
choice so the dashboard and desktop follow along. Absent/invalid = "stable".
|
|
*/
|
|
export async function getConfiguredUpdateChannel(): Promise<UpdateChannel> {
|
|
try {
|
|
const store = new GlobalSettingsStore();
|
|
await store.init();
|
|
const settings = await store.getSettings();
|
|
return settings.updateChannel === "beta" ? "beta" : "stable";
|
|
} catch {
|
|
return "stable";
|
|
}
|
|
}
|
|
|
|
export async function persistUpdateChannel(channel: UpdateChannel): Promise<void> {
|
|
const store = new GlobalSettingsStore();
|
|
await store.init();
|
|
const settings = await store.getSettings();
|
|
if (settings.updateChannel === channel) return;
|
|
await store.updateSettings({ updateChannel: channel });
|
|
}
|