# Release All workspaces share one version and release together. ## Two steps A release has exactly two steps. The agent does the first, the user authorizes the second. **Preparation** (local, reversible — agent does this): - format, lint, typecheck all green - resolve the release source to one commit and confirm that commit's existing CI is green - classify the diff from the previous stable to the release source as patch or minor, then show the target version and rationale to the user - draft the changelog, show it to the user, wait for review - run the pre-release sanity check, surface findings to the user **Go-ahead** (user says "go ahead"): - commit the approved release inputs locally - run the release, which publishes npm and pushes the prepared branch and tag - create the release heartbeat immediately and babysit it to completion Rules that apply to both steps: - Last-minute changes always need approval. Every time. - No code changes bundled into the changelog commit or the release commit. Code shims live in their own commit, reviewed on their own merits. - A sanity-check finding is information, not a directive. The agent surfaces it; the user decides. - Invoking a release skill is intent to start the flow, not blanket authorization to publish. - If the user asks for a release preview, show the prospective changelog/release contents and answer questions, but do not commit, tag, publish, or run release commands until they explicitly authorize the release. ## Release source and CI The default release source is `origin/main`. Fetch `origin`, then record the resolved commit. The default release checkout is a clean local `main` whose `HEAD` equals `origin/main`. An explicit user instruction can select another ref, such as a hotfix commit or tag. Resolve that ref once and apply every source, diff, and CI check to that commit instead of `origin/main`. Before making release-preparation commits, confirm the existing CI run for the resolved commit is green. Pending CI is watched to completion. Release preparation then stays local through the changelog, any explicitly requested ACP catalog update, lockfile preparation, and the version commit. After approval, commit the prepared inputs locally and run the release command. Its branch and tag push is the one remote release batch and starts CI for the complete release commit. ## Release branch discipline While you finalize a release on `main`, use a temporary `next` branch for work intended for the following release. This applies to both beta and stable releases. - Create each new `next` from freshly fetched `origin/main`. Reuse it while active. - "This goes to next" means create the PR against `next` or retarget an existing PR, and keep that destination through delivery. - Keep `next` current by merging `origin/main` into it as release fixes land. Avoid rebasing this shared branch because agents and open PRs depend on its history. - After the release ships, bring `next` up to date and open a `next` → `main` PR. Pass CI and merge without squashing away the individual PR commits needed for the changelog. Retarget remaining PRs based on `next` to `main` and delete the integrated `next`. Create it fresh when needed again. **Setup still needed:** CI, Docker, and Nix PR checks currently target only `main`, and GitHub permits only squash merges. Enable checks and required-check protection for `next`, CI on its pushes, and merge commits for the integration PR. Handle PR base changes (`edited` events) so retargeting runs checks against the new base; GitHub's default PR events do not cover this. Deployment triggers stay unchanged. ### Hotfix from a release tag If `main` contains changes you do not want to release, branch from the affected release tag and cherry-pick only the required fixes. Run CI on that branch, then use the normal release flow with it as the explicit source, choosing a new patch or beta version. Ensure the fixes and changelog also reach `main` and any active `next`, preserving newer development and version changes there. This is a short-lived hotfix branch, not another maintained release track. ## ACP catalog updates ACP catalog work enters a release through an explicit user request: - **Check ACP drift** — run `npm run acp:version-drift:check`. When drift exists, run `npm run acp:version-drift:update`, verify the catalog, and include the update in the local release-preparation commits. - **Update ACP** — run `npm run acp:version-drift:update`, verify the catalog, and include the update in the local release-preparation commits. The release authorization covers the requested ACP commit. It ships in the same release push as the changelog and version commit. ## Two paths There are two supported release paths: 1. **Direct stable release**: you are ready to ship the resolved release source to everyone immediately (default `origin/main`). 2. **Beta flow**: release candidates on the `beta` channel. Each beta folds into the series' single changelog entry, publishes npm only on the explicit `beta` dist-tag, and stays behind the Stable/Beta switch on `/download`. Paseo has one linear release track even though npm dist-tags are independent pointers. The npm invariant is: - A beta release moves only `beta`; `latest` remains on the newest stable. - A stable release moves both `latest` and `beta` to that stable version. This keeps users who install `@getpaseo/cli@beta` on the newest Paseo release after a beta is promoted or superseded by a direct stable release. ## Release version decision Every fresh release starts by classifying the full diff from the previous stable to the resolved release source. The highest-impact change determines the version: - **Minor** — a user would experience the release as a significant upgrade. This includes substantial new workflows, providers, forges, platforms, integrations, or meaningful expansions of existing capabilities. Foundational internal work also qualifies when it materially changes reliability, performance, compatibility, deployment, or operation; diff size alone does not. - **Patch** — fixes, polish, small enhancements, and reliability or performance improvements within existing capabilities. Follow-up corrections to a minor release are patches. The release agent selects patch or minor during preparation and presents the target version with the changelog for approval. Agents never select a major version autonomously. A major release requires an explicit user instruction and approval; Paseo remains on major version zero until that deliberate decision. Version bumps are never used to retry a failed build. Retry the existing version as described in **Fixing a failed release build**. ## Standard release (stable) Before running any stable release command: - Make sure the resolved release source passed CI, the approved release inputs are committed locally on the intended branch, and the working tree is clean. - **Run `npm run format`, `npm run lint`, and `npm run typecheck` and commit any resulting changes BEFORE you start any `release:*` command.** `release:check` runs `npm install --workspaces --include-workspace-root` as part of `release:prepare`, which can mutate `package-lock.json` (e.g. churning `"dev": true` markers on optional deps). The next step, `version:all:*`, runs `npm version` which aborts when the working tree is dirty. If this happens mid-flight you have to commit the lockfile churn before retrying — and the pre-commit format hook will reject a lockfile-only commit because oxfmt internally skips `package-lock.json` while lefthook's glob still matches it. Avoid the whole mess by running format/lint/typecheck first, then `release:prepare` once on its own to absorb any lockfile churn into a normal commit, then start the release. - Do not use a release command as a substitute for checking whether the current commit is actually ready. ```bash # Run exactly one, matching the approved decision: npm run release:patch npm run release:minor ``` This bumps the version across all workspaces, runs checks, publishes to npm, and pushes the branch + tag. The tag push triggers `Desktop Release`, `Android APK Release`, `Docker`, and `Release Notes Sync` on GitHub Actions. The workflows create the GitHub Release as a draft while builds and release-note sync run. EAS picks up the same tag via the EAS GitHub app and starts the iOS + Android store builds in parallel (see "Mobile builds (EAS)" below) — there is no mobile-release workflow under `.github/workflows`. After the stable release succeeds, move npm's `beta` pointer to the new stable version for every published package. This changes dist-tags only; do not republish the packages: ```bash PASEO_VERSION=$(node -p "require('./package.json').version") for package in highlight relay protocol client plugin server cli; do npm dist-tag add "@getpaseo/$package@$PASEO_VERSION" beta done ``` Verify both npm tags now resolve to `PASEO_VERSION` before considering the stable release complete. The Docker workflow builds images from the checked-out source tree on pull requests and on `main` as non-publishing checks. Stable `vX.Y.Z` tag pushes publish `ghcr.io/getpaseo/paseo:X.Y.Z` and `ghcr.io/getpaseo/paseo:latest`; beta `vX.Y.Z-beta.N` tag pushes publish only `ghcr.io/getpaseo/paseo:X.Y.Z-beta.N` and never move `latest`. The production relay is the Elixir service in [getpaseo/paseo-relay](https://github.com/getpaseo/paseo-relay), with its own deployment process. Paseo releases and pushes to this repository do not deploy it. The Cloudflare relay code and workflow in this repository are legacy and are not used in production. **Stable means stable.** If the user says "stable" or "ship stable", do not ask whether they want a beta first. They picked stable; treat it as a direct stable release. Only run the beta flow when the user explicitly says "beta". ## Manual step-by-step ```bash npm run typecheck # Verify the exact commit you intend to release npm run release:check # Typecheck, build, dry-run pack # Run exactly one approved version command: npm run version:all:patch npm run version:all:minor npm run release:publish # Publish to npm npm run release:push # Push HEAD + tag (triggers CI workflows) # Then move npm's beta dist-tag to this stable version using the command above. ``` ## Beta flow ```bash npm run release:beta:patch # Start the next patch beta line npm run release:beta:minor # Start the next minor beta line # ... test desktop and APK prerelease assets from GitHub Releases ... npm run release:beta:next # Optional: cut X.Y.Z-beta.2, beta.3, ... npm run release:promote # Promote X.Y.Z-beta.N to stable X.Y.Z ``` - Beta tags are published GitHub prereleases like `v0.1.41-beta.1` - Betas publish npm packages with `--tag beta`, so `npm install @getpaseo/cli@beta` opts in while plain `npm install @getpaseo/cli` stays on `latest` - Betas publish desktop assets and APKs for testing. They also build iOS, upload it to TestFlight, add it to the `Paseo Beta` external group, and submit it for Beta App Review. They do not submit mobile builds to the production stores. - `release:promote` creates a fresh stable tag like `v0.1.41`; the final release never reuses the beta tag - Desktop assets now come from the Electron package at `packages/desktop` - Require the Linux artifact CI checks with both restricted and usable user namespaces to pass before publication; see [packaged desktop smoke](testing.md#packaged-desktop-smoke). Keep the installed-package and AppImage checks together. - Beta releases use Electron's `beta` update channel. Users on the stable channel only receive stable releases; users on the beta channel receive beta releases and the final stable release when it is published. - **A beta series has one changelog entry.** Each beta folds into it and renames its heading to the new `## X.Y.Z-beta.N`, which `Release Notes Sync` mirrors into that prerelease body. Promotion renames it to the stable heading. See the Changelog policy section. Use the beta path when you need to: - smoke a build yourself before promoting it to everyone - test a build manually in a Linux or Windows VM - send a build to a user who is hitting a specific problem - iterate on `beta.1`, `beta.2`, `beta.3`, and so on before deciding to ship broadly ## Staged rollout (stable channel) Stable desktop releases go out via a linear time-based rollout for automatic update checks: 0% admitted when the updater manifests appear, 100% admitted 36 hours later, linear ramp in between. Manual checks bypass the rollout so a user can install immediately when they click **Check**. Beta releases bypass the rollout entirely — beta users always receive updates immediately. The rollout is driven by a `rolloutHours` field stamped into the GitHub Release manifests (`latest-mac.yml`, `latest-linux.yml`, `latest.yml`) by the `finalize-rollout` job in `desktop-release.yml`. Desktop release builds now publish in two phases: - The GitHub Release stays a draft while platform build jobs upload the installers/packages (`.dmg`, `.zip`, `.exe`, `.AppImage`, etc.). - The final job merges and stamps every channel manifest, uploads them with the final `releaseDate` and `rolloutHours`, then publishes the GitHub Release. Drafts do not appear in GitHub's releases feed. Updater clients continue to see the previous complete release until all three manifests are available. If a desktop build or manifest upload fails, the new release stays a draft. ### Default behavior `npm run release:patch` or `npm run release:minor` → tag push → 36h ramp. No extra action needed. The `rollout_hours` input on `desktop-release.yml` is **only read on `workflow_dispatch`** — tag-push runs always default to 36. To get any other rollout duration on a fresh release, use the post-publish flip below. ### Instant-admit release (rollout_hours=0 from publish) For a fresh release that should admit everyone immediately (low-risk change, doc-only, hotfix, or just a release you want out fast), cut the release normally and queue the rollout flip immediately after: ```bash # 1. Cut and publish (default 36h ramp from tag push). npm run release:patch # 2. Immediately queue the flip — runs as soon as finalize-rollout completes. gh workflow run desktop-rollout.yml \ -f tag=v0.1.64 \ -f rollout_hours=0 ``` **Why this is gap-free:** `desktop-release.yml`'s `finalize-rollout` job and `desktop-rollout.yml` share the concurrency group `desktop-rollout-`. Dispatching `desktop-rollout.yml` while the tag-push pipeline is still running queues it safely behind `finalize-rollout`. The first public manifests already carry `rolloutHours=36`, then `desktop-rollout.yml` flips them to `rolloutHours=0` shortly afterward. The renderer polls every 30 minutes, so active stable users pick up the new manifest on their next check. Run the dispatch right after `release:patch` or `release:minor` returns. Don't wait for the tag-push CI to finish. ### Adjusting an already-published release To change the rollout duration on a release that's already shipped — e.g. flip a hotfix to instant admit, or slow a release down — use the dedicated `desktop-rollout.yml` workflow. It edits the manifests in place on the GitHub release without rebuilding anything. It only rewrites `rolloutHours`; `releaseDate` is preserved, so the rollout clock keeps ticking from the original publish time. **Hotfix (instant admit) on an already-shipped release:** ```bash gh workflow run desktop-rollout.yml \ -f tag=v0.1.42 \ -f rollout_hours=0 ``` `rollout_hours=0` admits 100% of stable users on their next update check (within ~30 min for active clients). **Slow a rollout down** (e.g. extend total duration to 72h since the original release): ```bash gh workflow run desktop-rollout.yml \ -f tag=v0.1.42 \ -f rollout_hours=72 ``` `rollout_hours` is **total duration since the original release date**, not "extend by N more hours from now." If `v0.1.42` was published 2h ago and you set `rollout_hours=72`, the ramp finishes 70h from now. The dispatch is idempotent and shares the `desktop-rollout-` concurrency group with `desktop-release.yml`'s `finalize-rollout` job, so it serializes safely against an in-flight tag-push pipeline targeting the same release. ### Custom ramp on a manually-dispatched build `desktop-release.yml` accepts `rollout_hours` only on `workflow_dispatch`, which is the path used to **rebuild an existing tag** (retry a failed release, force a rebuild on a different ref). When you go that route, you can stamp a non-default ramp directly: ```bash gh workflow run desktop-release.yml \ -f tag=v0.1.43 \ -f rollout_hours=6 ``` This does **not** apply to fresh releases cut via `npm run release:patch` or `npm run release:minor` — those paths always tag-push and stamp 36. For a fresh release with a custom ramp, cut normally and then dispatch `desktop-rollout.yml` (same pattern as the instant-admit flow above, with your chosen `rollout_hours`). ### Releasing during an active rollout If you ship N+1 while N is still ramping, N+1 starts a fresh rollout from its own publish timestamp. N's rollout effectively ends — the newer manifest supersedes it. Rollout-aware clients revalidate the manifest for up to five seconds before installing a downloaded update on quit. If N+1 has replaced N but the client is not admitted to N+1 yet, it skips the downloaded N and waits rather than installing two updates in succession. If revalidation times out, the app exits without installing the cached update. If N+1 is a hotfix for a bug in N, dispatch `desktop-rollout.yml -f tag=v0.1. -f rollout_hours=0` after N+1 publishes so the users who already got N reach the fix fast. ### macOS system floor The desktop app requires macOS 13 or newer. Keep both release guards when the floor changes: - `packages/desktop/electron-builder.yml` writes the macOS version to `LSMinimumSystemVersion` for new installs. - `scripts/merge-mac-manifest.mjs` writes the matching Darwin kernel version to `minimumSystemVersion` in the update manifest. Existing clients check this before downloading an update. macOS 13 maps to Darwin 22. The two values use different version domains; do not copy the macOS version into the update manifest. ### Limitations - **No pause / kill switch.** To stop new admissions, ship a superseding release. Clients revalidate on quit and will not install the superseded download, but a client that already completed installation cannot be recalled; ship a hotfix `+1` patch. - **No rollback.** `allowDowngrade = false`. Bad release = ship a hotfix. - **Bootstrap caveat.** Clients running a build older than the rollout feature ignore `rolloutHours` and admit immediately. Rollout protection only applies to clients running the rollout-aware version or later. - **Up to ~30 min automatic admission latency.** Renderer polls every 30 minutes, so a stable user may take up to that long to be evaluated against the rollout window. Clicking **Check** is manual and bypasses rollout admission. ## Mobile builds (EAS) iOS and Android store builds are not in `.github/workflows`. They are triggered by the EAS GitHub app the moment the `v*` tag is pushed: - **Android (Play Store)** — EAS builds with profile `production` and auto-submits to the Play Store via `eas submit` (EAS-managed credentials, no Fastlane). - **iOS (TestFlight + App Store)** — EAS builds with profile `production`, uploads to TestFlight, and a Fastlane lane submits the build for App Store review. - **Android APK (GitHub Release asset)** — separate, via `.github/workflows/android-apk-release.yml`. This is the only Android-related workflow that lives in this repo. EAS uses the local app version source. `packages/app/app.config.js` derives the native version from the package version. Android `versionCode` is `major * 1_000_000 + minor * 1_000 + patch`. iOS reserves 1,000 build slots per app version: beta `N` uses slot `N`, and stable uses slot `999`. For example, `0.2.6-beta.2` appears in App Store Connect as version `0.2.6` build `2006002`; stable uses build `2006999`. Rebuilding the same tag produces the same native build number; if a store has already accepted a binary and you need a different binary, cut the next beta or patch instead of relying on EAS remote auto-increment. Beta tags run `Release iOS Beta`. The workflow uploads the build to TestFlight, distributes it to the persistent `Paseo Beta` external group, and submits it for Beta App Review. Testers and the group are managed once in App Store Connect; releases require no dashboard action. There is no mobile-release workflow under `.github/workflows`. The EAS GitHub app reads the workflows under `packages/app/.eas/workflows` and handles tag triggering directly. ### Watching mobile builds from the terminal Use the EAS CLI from `packages/app/`: ```bash cd packages/app # Recent builds (newest first). Pipe to jq for status only. npx eas build:list --limit 8 --non-interactive --json | jq '.[] | {platform, status, appVersion, gitCommitHash}' # Recent EAS workflow runs. This is the source of truth for submit/review jobs. npx eas workflow:runs --json | jq '.[] | {status, workflowName, trigger, gitCommitHash, startedAt, finishedAt}' # Filter by platform. npx eas build:list --platform ios --limit 5 --non-interactive --json npx eas build:list --platform android --limit 5 --non-interactive --json # Inspect a specific build. npx eas build:view # Inspect the full release workflow, including submit_ios, submit_android, # and submit_ios_for_review. npx eas workflow:view --json # Read failed submit/review job logs. npx eas workflow:logs --all-steps --non-interactive # Stream logs for a build. npx eas build:view --json | jq '.logFiles[]' ``` A build's `gitCommitHash` must match the release tag commit. `status` walks through `NEW` → `IN_QUEUE` → `IN_PROGRESS` → `FINISHED` (or `ERRORED`/`CANCELED`). The EAS workflow run's `gitCommitHash` and `trigger` must also match the release tag. Once a build is `FINISHED`, EAS still has release-critical work to do: Android must submit to the Play Store, and iOS must upload to TestFlight **and** submit the build for App Store review. The release is not done until all platforms are on their way through the stores. For the `Release Mobile` EAS workflow, these jobs must pass: - `build_ios` — iOS binary built - `submit_ios` — iOS binary uploaded to App Store Connect/TestFlight - `submit_ios_for_review` — iOS build submitted for App Store review via Fastlane - `build_android` — Android store binary built - `submit_android` — Android binary submitted to the Play Store Do not treat `build_ios: SUCCESS` or `submit_ios: SUCCESS` as a completed iOS release. `submit_ios_for_review: FAILURE` means the iOS release is blocked even if the build is visible in TestFlight. To confirm the submission landed, inspect the EAS workflow with `npx eas workflow:view --json`. App Store Connect (review state for the matching version/build) and the Play Console track are the final ground truth. ## Release completion and heartbeat A release is **in progress** after npm publication and tag push. Report it as **shipped** only after every applicable build, publication, asset, manifest, and store submission passes the completion checklist. Immediately after every beta, stable, or promotion tag push, create a heartbeat that resumes the release in the current conversation. Create it automatically with `create_heartbeat`. The heartbeat owns the release until it either reaches the completion checklist or finds a failure that needs new user authority. Each heartbeat checks the release tag commit, all GitHub Actions runs for the release branch and tag, npm dist-tags, the GitHub Release body and assets, desktop updater manifests, the published Docker image, and the applicable EAS workflow. Inspect the GitHub Release itself and confirm that the macOS, Linux, Windows, and Android APK assets are present along with the channel manifests (`latest-mac.yml`, `latest-linux.yml`, and `latest.yml` for stable; `beta-mac.yml`, `beta-linux.yml`, and `beta.yml` for beta). For stable releases, also confirm every required mobile build, upload, store submission, and review-submission job for the release commit. For betas, confirm the beta EAS workflow completed its TestFlight distribution and Beta App Review path. Delete the heartbeat only after every applicable checklist item passes, then report the release as shipped. Pattern: ```jsonc // mcp__paseo__create_heartbeat arguments { "name": "vX.Y.Z release babysit heartbeat", "cron": "*/10 * * * *", "timezone": "UTC", "maxRuns": 120, "expiresIn": "24h", "prompt": "Resume the vX.Y.Z release babysit for commit . Check npm tags; every GitHub Actions run for the release branch and tag; the published GitHub Release body, expected desktop/APK assets, and channel manifests; the Docker image; and the matching EAS workflow. Completion requires every applicable checklist item. For stable, require build_ios, submit_ios, submit_ios_for_review, build_android, and submit_android to succeed. For beta, require the beta TestFlight distribution and Beta App Review path. If work is pending, wait for the next heartbeat. If a failure can be retried safely for the same version, follow the failed-release procedure; otherwise report the blocker. When every applicable completion-checklist item passes, delete THIS heartbeat, report shipped, and stop.", } ``` Run an immediate status check after creating the heartbeat. The heartbeat handles later transitions and stops itself when the release is complete. ## Release notes on GitHub The GitHub Release body is populated automatically by the `Release Notes Sync` workflow (`.github/workflows/release-notes-sync.yml`). It triggers on every `v*` tag push and on any push to `main` that touches `CHANGELOG.md`, then runs `scripts/sync-release-notes-from-changelog.mjs` to mirror the matching changelog entry into the release body. You don't need to write release notes on GitHub manually — keep `CHANGELOG.md` correct and the workflow will sync it. To force a re-sync, dispatch the workflow with the tag input. ## Website behavior - The website download page defaults to GitHub's latest published **stable** release. - A published beta prerelease is offered behind the Stable/Beta switch on `/download` (`?channel=beta`), never as the default. The switch only appears while the newest prerelease leads stable on its core version, so promoting `X.Y.Z-beta.N` to `X.Y.Z` retires the beta channel from the page until the next beta line opens. - Homebrew, the Play Store, the App Store, and `app.paseo.sh` have no beta. The Beta view drops those rows, and the whole Web section, rather than showing an inert "stable only" placeholder. When a surface gains a beta path — say a public TestFlight link — add its row back in `packages/website/src/routes/download.tsx`. - The default download target only moves when you publish the final stable release tag like `v0.1.41`. - The public `/changelog` page renders `CHANGELOG.md` as-is, so the in-flight `-beta.N` entry shows there once it lands on `main` — that's intended, it's where beta users check what's coming. Only the **default download target** stays pinned to the latest stable; the download links read GitHub's releases API, not the changelog, so a `-beta.N` heading on top never affects them. - The download page's "What's new" link deep-links the **minor group** anchor (`/changelog#release-0.3`), not the exact entry: the beta entry is renamed on each beta and on promotion, so the minor group remains the durable target. A version with no entry in the bundled changelog — a tag whose changelog commit hasn't redeployed the site yet — links the plain `/changelog` instead of a dead anchor. - The website itself is deployed by `Deploy Website` (Cloudflare Workers), which redeploys on the `release: published` event emitted when a stable draft is published and on pushes to `main` that touch `CHANGELOG.md` or `packages/website/**`. Its job condition excludes beta prereleases. ## Fixing a failed release build **NEVER bump the version to fix a build problem.** New versions are reserved for meaningful product changes (features, fixes, improvements). Build/CI failures are fixed on the current version. **Do not rely on `workflow_dispatch` for tagged code fixes.** The `workflow_dispatch` trigger runs the workflow file from the default branch but checks out the code at the tag ref (`ref: ${{ inputs.tag }}`). That means fixes committed to `main` won't change the tagged source tree being built. `workflow_dispatch` only helps when the fix lives in the workflow file itself. For Docker-only retries, **do not push or force-push a `v*` release tag**. `v*` tag pushes rebuild desktop assets, the Android APK, Docker, release notes, and EAS mobile release builds. Use the Docker workflow dispatch instead: ```bash gh workflow run docker.yml \ --ref main \ -f paseo_version=X.Y.Z-beta.N \ -f publish=true ``` This replaces `ghcr.io/getpaseo/paseo:X.Y.Z-beta.N` in place without touching desktop, APK, or EAS release builders. The Docker exception is safe because the dispatch runs from `--ref main` and uses the explicit `paseo_version`; it does not check out or move the `v*` release tag. To retry a failed non-Docker release workflow, push a retry tag on the commit you want to build. Reusing the same tag name is expected: move it with `git tag -f ...` and push it with `--force` so the workflow rebuilds the commit you actually want. A failed desktop build leaves the GitHub Release as a draft. `finalize-rollout` uploads manifests from successful platforms before it fails. A later single-platform retry reuses those manifests, stamps the complete set with one release date, and publishes the draft. Use `desktop-vX.Y.Z` when more than one platform failed. A `workflow_dispatch` rebuild with publishing enabled follows the same path against the existing draft. Prefer a tag push over `workflow_dispatch` when rebuilding desktop or APK release assets. Prefer Docker workflow dispatch when rebuilding only the Docker image. The retry tag patterns below still work and remain the supported way to rebuild specific release targets: ```bash # Desktop (all platforms) git tag -f desktop-v0.1.28 HEAD && git push origin desktop-v0.1.28 --force # Desktop (single platform) git tag -f desktop-macos-v0.1.28 HEAD && git push origin desktop-macos-v0.1.28 --force git tag -f desktop-linux-v0.1.28 HEAD && git push origin desktop-linux-v0.1.28 --force git tag -f desktop-windows-v0.1.28 HEAD && git push origin desktop-windows-v0.1.28 --force # Android APK git tag -f android-v0.1.28 HEAD && git push origin android-v0.1.28 --force # Beta git tag -f v0.1.29-beta.2 HEAD && git push origin v0.1.29-beta.2 --force ``` This ensures the checkout ref matches the actual code on `main` with the fix included. - `vX.Y.Z` or `vX.Y.Z-beta.N` rebuilds the full tagged release - `desktop-vX.Y.Z` rebuilds desktop for all desktop platforms only - `desktop-macos-vX.Y.Z`, `desktop-linux-vX.Y.Z`, and `desktop-windows-vX.Y.Z` rebuild only that desktop platform - `android-vX.Y.Z` rebuilds the Android APK release only If you decide to publish a release without working desktop builds, inspect its assets first, then publish it manually: ```bash RELEASE_LOOKUP=$(node scripts/github-release.mjs --repo getpaseo/paseo --tag vX.Y.Z) gh release view "$RELEASE_LOOKUP" --json isDraft,isPrerelease,assets gh release edit "$RELEASE_LOOKUP" --tag vX.Y.Z --draft=false # Keep a beta marked as a prerelease: RELEASE_LOOKUP=$(node scripts/github-release.mjs --repo getpaseo/paseo --tag vX.Y.Z-beta.N) gh release edit "$RELEASE_LOOKUP" --tag vX.Y.Z-beta.N --draft=false --prerelease ``` This bypasses the updater-manifest guarantee. Use it only when the release is intentionally unavailable to desktop updater clients. ## Notes - `version:all:*` bumps root + syncs workspace versions and `@getpaseo/*` dependency versions - The npm `version` lifecycle regenerates F-Droid changelog files from `CHANGELOG.md` for stable releases only (`npm run fdroid:changelogs`) and stages them, so the release tag carries them. Betas are a no-op. A stable run **aborts the release** if `CHANGELOG.md` has no entry for the version being cut — commit the changelog entry first. See [docs/android.md](android.md) for why these files are generated per ABI. - `release:prepare` refreshes workspace `node_modules` links to prevent stale types - `npm run dev:desktop` and `npm run build:desktop` target the Electron desktop package in `packages/desktop` - If `release:publish` partially fails, re-run it — npm skips already-published versions - If `release:publish:beta` partially fails, re-run it — npm skips already-published versions and keeps prereleases off `latest` because every publish uses `--tag beta` - The website uses GitHub's latest published release API for download links, so published beta prereleases do not replace the stable download target. ## Changelog format Release notes depend on the changelog heading format. The heading **must** be strictly followed: ``` ## X.Y.Z - YYYY-MM-DD ## X.Y.Z-beta.N - YYYY-MM-DD ``` No prefix (`v`), no extra text. `Release Notes Sync` matches the `## X.Y.Z` (or `## X.Y.Z-beta.N`) line for the pushed tag to extract the version. A malformed heading breaks the release-notes sync for that tag. `CHANGELOG.md` on `main` is also what the app's **What's new** sheet fetches and renders, so the file is a shipped product surface, not just a release input. `##` starts a release and `###` starts a section; the app reads section titles from the document, so renaming or adding one needs no app change. Everything under a section is rendered as Markdown: prose, lists, links, inline code, fenced code, block quotes, tables, and images. Raw HTML does not render — the shared Markdown parser runs with `html: false`, so a `