## 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>
209 lines
12 KiB
Markdown
209 lines
12 KiB
Markdown
# Releasing
|
||
|
||
This project uses [changesets](https://github.com/changesets/changesets) for automated versioning and release management. Releases are distributed through two channels:
|
||
|
||
1. **npm packages** — published automatically via `version.yml` using changesets
|
||
2. **GitHub Release with platform binaries** — built and uploaded via `release.yml` when a version tag is pushed
|
||
|
||
## How it works
|
||
|
||
### 1. Add a changeset
|
||
|
||
When you make a change that should be included in a release, add a changeset:
|
||
|
||
```bash
|
||
pnpm changeset
|
||
```
|
||
|
||
This will prompt you to:
|
||
- Select which packages are affected
|
||
- Choose the semver bump type (patch, minor, major)
|
||
- Write a summary of the change
|
||
|
||
Then edit the created changeset file to use the structured body format:
|
||
|
||
```markdown
|
||
---
|
||
"@runfusion/fusion": minor
|
||
---
|
||
|
||
summary: Add a Command Center productivity control for LOC backfills.
|
||
category: feature
|
||
dev: Uses the new `fn_backfill_loc` tool; settings key `commandCenter.locBackfill`.
|
||
```
|
||
|
||
Fields:
|
||
- `summary` (required) — one line, user-facing, max 120 chars.
|
||
- `category` (required) — one of: `feature`, `fix`, `breaking`, `security`, `performance`, `internal`.
|
||
- `dev` (optional) — developer/migration detail.
|
||
|
||
A markdown file will be created in the `.changeset/` directory. Commit this file along with your code changes. Validate with `pnpm check:changesets`.
|
||
|
||
### 2. Version PR is created automatically
|
||
|
||
When changesets are merged to `main`, the `version.yml` workflow automatically opens (or updates) a **"Version Packages"** pull request. This PR:
|
||
|
||
- Consumes all pending changeset files
|
||
- Bumps package versions according to the changeset declarations
|
||
- Generates/updates `CHANGELOG.md` files for affected packages
|
||
- Distills the version's changeset summaries into grouped, end-user-facing release notes in the root `CHANGELOG.md` via Claude (`claude -p --model sonnet`): a **Highlights** section (top 3–5) plus category groups, and prints an engagement-oriented X draft (≤280 chars) after a local release
|
||
|
||
### 3. Merge the Version PR to release
|
||
|
||
When you merge the Version Packages PR:
|
||
|
||
- The `version.yml` workflow detects that all changesets have been consumed
|
||
- It builds all packages and publishes them to **npm** with provenance attestation
|
||
- It creates a git tag `v{version}` based on the `kb` CLI package version
|
||
- The tag push triggers `release.yml`, which:
|
||
- Builds platform-specific binaries for Linux x64, macOS x64, macOS arm64, and Windows x64
|
||
- Builds Android release assets: signed `fusion-android-release.apk` + `fusion-android-release.aab` when Android signing secrets are configured, otherwise unsigned debug `fusion-android.apk`
|
||
- Signs macOS binaries (codesign + notarization), Windows binaries (Authenticode), and Android release artifacts when keystore secrets are available
|
||
- Generates SHA256 checksums for all binaries and Android artifacts
|
||
- Creates a **GitHub Release** with all binaries, Android artifacts, and checksums attached
|
||
|
||
## Release tracks: beta and stable
|
||
|
||
Fusion ships on two tracks. Users pick theirs with the `updateChannel` global setting (Settings → General → Release channel) or `fn update --channel <stable|beta>`.
|
||
|
||
| Track | Cut from | Version shape | npm dist-tag | GitHub Release | Homebrew |
|
||
|-------|----------|---------------|--------------|----------------|----------|
|
||
| beta | `main` | `X.Y.Z-beta.N` | `beta` | prerelease | — |
|
||
| stable | `release` branch | `X.Y.Z` | `latest` | latest | bumped |
|
||
|
||
### Beta release (from `main`)
|
||
|
||
```bash
|
||
pnpm release # prompts for the channel; beta is the default answer
|
||
pnpm release --channel beta # explicit, no prompt
|
||
```
|
||
|
||
The script auto-enters changesets pre-mode (`.changeset/pre.json`, tag `beta`) the first time, versions to the next `-beta.N`, publishes with an explicit `--tag beta`, tags `vX.Y.Z-beta.N` (the tag push builds binaries and marks the GitHub Release as a prerelease), and skips the Homebrew tap and X draft. Changeset `.md` files are *preserved* through beta versioning — pre-mode records them in `pre.json` so the eventual stable release aggregates everything.
|
||
|
||
### Promoting to stable
|
||
|
||
The easy path — run from `main` and let the script do the promotion:
|
||
|
||
```bash
|
||
pnpm release --channel stable # or answer "stable" at the channel prompt
|
||
```
|
||
|
||
When run from `main` with the stable channel, the script starts **assisted promotion**:
|
||
|
||
1. It proposes the newest `v*-beta*` tag reachable from HEAD as the promotion target (promote a tested beta, not main's tip); you can accept or type another tag/commit.
|
||
2. It verifies `release` fast-forwards cleanly to that commit (a diverged release branch — e.g. unmerged hotfixes — fails with instructions instead of guessing).
|
||
3. It creates a **temporary git worktree** on `release` at the target (bootstrapping the branch if it doesn't exist yet), installs dependencies there, and re-runs the whole stable release inside it — your checkout never leaves `main`. The Homebrew tap path is handed into the worktree via `FUSION_HOMEBREW_TAP_DIR`.
|
||
4. On success the temp worktree is removed; on failure it is kept for inspection.
|
||
|
||
The stable release itself exits pre-mode, versions to the clean `X.Y.Z` with the aggregated changelog, publishes to `latest`, marks the GitHub Release latest, and bumps the Homebrew tap.
|
||
|
||
Afterwards, **back-merge `release` into `main`** (the script prints the exact commands). This carries the consumed changesets, changelogs, version bump, and pre.json removal back — without it, the next beta double-releases old changesets.
|
||
|
||
Manual alternative: check out `release` in a worktree yourself, `git merge --ff-only vX.Y.Z-beta.N`, and run `pnpm release --channel stable` there.
|
||
|
||
### Hotfixes
|
||
|
||
Commit on `release` (or a worktree branched from it), add a changeset, run `pnpm release`, then cherry-pick the fix back to `main`.
|
||
|
||
### Channel semantics for users
|
||
|
||
- `stable` follows the npm `latest` dist-tag only — betas are invisible.
|
||
- `beta` follows the semver-max of `latest` and `beta`, so beta users are offered each promoted stable once it overtakes their prerelease.
|
||
- Switching beta → stable never downgrades; the user stays on the installed beta until the next stable passes it. `fn update --channel stable --force` is the explicit downgrade.
|
||
- Desktop beta builds emit `beta*.yml` electron-updater manifests; the desktop app selects them when `updateChannel` is `beta` (`allowPrerelease` + channel).
|
||
|
||
## Distribution channels
|
||
|
||
| Channel | Workflow | Trigger | Output |
|
||
|---------|----------|---------|--------|
|
||
| npm | `version.yml` (stable only) or `pnpm release` | Manual | npm packages with provenance (CI) |
|
||
| GitHub Release | `release.yml` | Version tag (`v*`; `v*-beta.N` → prerelease) | Signed platform binaries, Android APK/AAB + checksums |
|
||
|
||
## Platform binaries
|
||
|
||
| Platform | Binary name | Signed |
|
||
|----------|------------|--------|
|
||
| Linux x64 | `fusion-linux-x64` | — |
|
||
| macOS arm64 | `fusion-darwin-arm64` | ✓ (codesign + notarization) |
|
||
| Windows x64 | `fusion-windows-x64.exe` | ✓ (Authenticode) |
|
||
| Android | `fusion-android-release.apk`, `fusion-android-release.aab` | ✓ when Android keystore secrets are configured |
|
||
| Android fallback | `fusion-android.apk` | — (debug/unsigned APK when Android keystore secrets are absent) |
|
||
|
||
> macOS Intel (`darwin-x64`) is intentionally not shipped: the CLI is Apple-Silicon-only because `macos-13` GitHub runners are too scarce to build reliably. The desktop macOS DMG/ZIP remains universal.
|
||
|
||
## Android release signing
|
||
|
||
`release.yml` and the tag-less `test-release.yml` rehearsal workflow publish signed Android release artifacts when all Android signing secrets are configured:
|
||
|
||
- `ANDROID_KEYSTORE_BASE64` — base64-encoded `.jks` / `.keystore` file
|
||
- `ANDROID_KEYSTORE_PASSWORD`
|
||
- `ANDROID_KEY_ALIAS`
|
||
- `ANDROID_KEY_PASSWORD`
|
||
|
||
Encode the keystore before saving it as a GitHub Actions secret:
|
||
|
||
```bash
|
||
base64 -w0 release.keystore
|
||
```
|
||
|
||
The Android native project under `packages/mobile/android/` is generated and gitignored, so CI does not commit signing configuration into Gradle files. Instead, the release job injects signing at build time with Android Gradle Plugin `android.injected.signing.*` properties, builds `assembleRelease` and `bundleRelease`, verifies the APK signature, and uploads `fusion-android-release.apk`, `fusion-android-release.aab`, and matching `.sha256` files. If `ANDROID_KEYSTORE_BASE64` is absent, the workflow preserves the secret-free path by building the unsigned debug APK as `fusion-android.apk` with `fusion-android.apk.sha256`.
|
||
|
||
Automated Play Store / Play Console upload is intentionally out of scope for the release pipeline right now. It needs a Google service-account JSON secret, a published Play listing, and fastlane or `r0adkll/upload-google-play` wiring; that work is tracked separately in FN-7043 while Fusion remains sideload-first for pre-1.0 Android distribution.
|
||
|
||
## Testing binary builds
|
||
|
||
Use the **Test Release** workflow (`test-release.yml`) to manually test binary builds without creating a real release:
|
||
|
||
1. Go to **Actions** → **Test Release** → **Run workflow**
|
||
2. The workflow builds all 4 platform binaries plus the Android APK/AAB path (signed when Android signing secrets are available, unsigned debug APK otherwise), runs smoke tests, and uploads artifacts
|
||
3. Download the `all-binaries` artifact to inspect the output
|
||
|
||
## Manual release (fallback)
|
||
|
||
If you need to release manually, you can still push a version tag directly:
|
||
|
||
```bash
|
||
git tag v0.2.0
|
||
git push origin v0.2.0
|
||
```
|
||
|
||
This will trigger `release.yml` to build binaries and create a GitHub Release. Note: npm publishing is handled separately by `version.yml` and won't be triggered by a manual tag push.
|
||
|
||
## Available scripts
|
||
|
||
| Script | Description |
|
||
|--------|-------------|
|
||
| `pnpm changeset` | Add a new changeset |
|
||
| `pnpm changeset status` | Check pending changesets |
|
||
| `pnpm release` | Local interactive release: previews changesets, lets you accept or override the proposed version, then bumps + builds + publishes + tags; Claude authors Highlights + a ≤280-char engagement X draft (soft deterministic fallback if Claude is offline) |
|
||
| `pnpm release --yes` | Same, but auto-accepts the proposed version and skips the final confirmation |
|
||
| `pnpm release --dry-run` | Preview only — show changesets, proposed version, and Claude-authored X draft preview, then exit before any file/git/npm changes |
|
||
| `pnpm release --channel beta` | Beta release from `main`: pre-mode version `X.Y.Z-beta.N`, npm dist-tag `beta`, GitHub prerelease; no Homebrew/X draft |
|
||
| `pnpm release --channel stable` | Stable release: requires the `release` branch, publishes `latest`, marks the GitHub Release latest |
|
||
|
||
Without `--channel`, `pnpm release` prompts for the channel and **defaults to beta** (also the silent default with `--yes` or a non-interactive dry-run). Stable releases are always an explicit choice.
|
||
| `pnpm release:version` | Apply changesets and bump versions (used by CI) |
|
||
| `pnpm --filter @runfusion/fusion build:exe` | Build binary for current platform |
|
||
| `pnpm --filter @runfusion/fusion build:exe -- --target <target>` | Cross-compile for a specific platform |
|
||
| `pnpm --filter @runfusion/fusion build:exe:all` | Build binaries for all platforms |
|
||
|
||
## Tips
|
||
|
||
- Every user-facing change should have a changeset — CI will remind you if one is missing
|
||
- You can add multiple changesets per PR if you're making changes to multiple packages
|
||
- Changeset files are automatically deleted when versions are bumped
|
||
- CI verifies binary compilation on every push/PR to catch build regressions early
|
||
- If your project enables `completionDocumentationMode: "changeset"`, triage specs will explicitly require `.changeset/*.md` completion artifacts for relevant tasks; keep this aligned with your repo's release convention.
|
||
|
||
## Internal packages
|
||
|
||
The following packages are **internal** and are **not published to npm**:
|
||
|
||
- `@fusion/core` — Core domain model and task store
|
||
- `@fusion/dashboard` — Web UI and API server
|
||
- `@fusion/engine` — AI agents and orchestration
|
||
- `@fusion/plugin-sdk` — Plugin development SDK
|
||
- `@fusion-plugin-examples/*` — Example plugins
|
||
|
||
These packages have `private: true` in their `package.json` and are listed in the `.changeset/config.json` `ignore` array to prevent accidental publishing. Only the `@runfusion/fusion` package is published to npm.
|