feat: add beta release channel

This commit is contained in:
Mohamed Boudra
2026-04-20 20:38:56 +07:00
parent 831d990ef7
commit dfc814147a
32 changed files with 442 additions and 256 deletions

View File

@@ -4,9 +4,9 @@ on:
push: push:
tags: tags:
- 'v*' - 'v*'
- '!v*-rc.*' - '!v*-beta.*'
- 'app-v*' - 'app-v*'
- '!app-v*-rc.*' - '!app-v*-beta.*'
workflow_dispatch: workflow_dispatch:
jobs: jobs:

View File

@@ -145,6 +145,7 @@ jobs:
if [[ "$IS_SMOKE_TAG" != "true" ]]; then if [[ "$IS_SMOKE_TAG" != "true" ]]; then
publish_mode="always" publish_mode="always"
publish_args+=("-c.publish.releaseType=$RELEASE_TYPE") publish_args+=("-c.publish.releaseType=$RELEASE_TYPE")
publish_args+=("-c.publish.channel=$RELEASE_CHANNEL")
fi fi
npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --mac --${{ matrix.electron_arch }} "${publish_args[@]}" npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --mac --${{ matrix.electron_arch }} "${publish_args[@]}"
@@ -154,7 +155,7 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: mac-manifest-${{ matrix.electron_arch }} name: mac-manifest-${{ matrix.electron_arch }}
path: ${{ env.DESKTOP_PACKAGE_PATH }}/release/latest-mac.yml path: ${{ env.DESKTOP_PACKAGE_PATH }}/release/${{ env.RELEASE_CHANNEL }}-mac.yml
retention-days: 1 retention-days: 1
finalize-mac-manifest: finalize-mac-manifest:
@@ -227,8 +228,9 @@ jobs:
return out; return out;
} }
const arm64Text = fs.readFileSync('mac-manifest-arm64/latest-mac.yml', 'utf8'); const manifestName = `${process.env.RELEASE_CHANNEL}-mac.yml`;
const x64Text = fs.readFileSync('mac-manifest-x64/latest-mac.yml', 'utf8'); const arm64Text = fs.readFileSync(`mac-manifest-arm64/${manifestName}`, 'utf8');
const x64Text = fs.readFileSync(`mac-manifest-x64/${manifestName}`, 'utf8');
const arm64 = parseManifest(arm64Text); const arm64 = parseManifest(arm64Text);
const x64 = parseManifest(x64Text); const x64 = parseManifest(x64Text);
@@ -243,7 +245,7 @@ jobs:
}; };
const output = toYaml(merged); const output = toYaml(merged);
fs.writeFileSync('latest-mac.yml', output); fs.writeFileSync(manifestName, output);
console.log('Merged manifest:\n' + output); console.log('Merged manifest:\n' + output);
NODE NODE
@@ -251,7 +253,7 @@ jobs:
if: env.IS_SMOKE_TAG != 'true' if: env.IS_SMOKE_TAG != 'true'
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "$RELEASE_TAG" latest-mac.yml --clobber --repo "${{ github.repository }}" run: gh release upload "$RELEASE_TAG" "$RELEASE_CHANNEL-mac.yml" --clobber --repo "${{ github.repository }}"
publish-linux: publish-linux:
needs: [create-release] needs: [create-release]
@@ -316,6 +318,7 @@ jobs:
if [[ "$IS_SMOKE_TAG" != "true" ]]; then if [[ "$IS_SMOKE_TAG" != "true" ]]; then
publish_mode="always" publish_mode="always"
publish_args+=("-c.publish.releaseType=$RELEASE_TYPE") publish_args+=("-c.publish.releaseType=$RELEASE_TYPE")
publish_args+=("-c.publish.channel=$RELEASE_CHANNEL")
fi fi
npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --linux --x64 "${publish_args[@]}" npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --linux --x64 "${publish_args[@]}"
@@ -391,6 +394,7 @@ jobs:
if [[ "$IS_SMOKE_TAG" != "true" ]]; then if [[ "$IS_SMOKE_TAG" != "true" ]]; then
publish_mode="always" publish_mode="always"
publish_args+=("-c.publish.releaseType=$RELEASE_TYPE") publish_args+=("-c.publish.releaseType=$RELEASE_TYPE")
publish_args+=("-c.publish.channel=$RELEASE_CHANNEL")
fi fi
npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --win --x64 "${publish_args[@]}" npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --win --x64 "${publish_args[@]}"

View File

@@ -1,5 +1,10 @@
# Changelog # Changelog
## 0.1.60-beta.1 - 2026-04-20
### Added
- Beta release channel for desktop updates, with a Settings toggle for receiving beta builds before they are promoted to stable.
## 0.1.59 - 2026-04-16 ## 0.1.59 - 2026-04-16
### Added ### Added

View File

@@ -51,7 +51,7 @@ Stable tag pushes like `v0.1.0` trigger:
- `packages/app/.eas/workflows/release-mobile.yml` on Expo servers (iOS + Android build + submit) - `packages/app/.eas/workflows/release-mobile.yml` on Expo servers (iOS + Android build + submit)
- `.github/workflows/android-apk-release.yml` on GitHub Actions (APK asset on GitHub Release) - `.github/workflows/android-apk-release.yml` on GitHub Actions (APK asset on GitHub Release)
Release candidate tags like `v0.1.1-rc.1` only trigger the GitHub APK workflow. They publish a GitHub prerelease APK for testing and do not submit to the stores. Beta tags like `v0.1.1-beta.1` only trigger the GitHub APK workflow. They publish a GitHub prerelease APK for testing and do not submit to the stores.
### Useful commands ### Useful commands

View File

@@ -7,7 +7,7 @@ All workspaces share one version and release together.
There are two supported ways to ship from `main`: There are two supported ways to ship from `main`:
1. **Direct stable release**: you are ready to ship the current `main` commit to everyone immediately. 1. **Direct stable release**: you are ready to ship the current `main` commit to everyone immediately.
2. **Release candidate flow**: you want public test builds first, but you are not ready for the website, npm, or production mobile release flows to move yet. 2. **Beta flow**: you want public test builds first, but you are not ready for the website, npm, or production mobile release flows to move yet.
## Standard release (patch) ## Standard release (patch)
@@ -37,31 +37,32 @@ npm run release:publish # Publish to npm
npm run release:push # Push HEAD + tag (triggers CI workflows) npm run release:push # Push HEAD + tag (triggers CI workflows)
``` ```
## Release candidate flow ## Beta flow
```bash ```bash
npm run release:rc:patch # Bump to X.Y.Z-rc.1, push commit + tag npm run release:beta:patch # Bump to X.Y.Z-beta.1, push commit + tag
# ... test desktop and APK prerelease assets from GitHub Releases ... # ... test desktop and APK prerelease assets from GitHub Releases ...
npm run release:rc:next # Optional: cut X.Y.Z-rc.2, rc.3, ... npm run release:beta:next # Optional: cut X.Y.Z-beta.2, beta.3, ...
npm run release:promote # Promote X.Y.Z-rc.N to stable X.Y.Z npm run release:promote # Promote X.Y.Z-beta.N to stable X.Y.Z
``` ```
- RC tags are published GitHub prereleases like `v0.1.41-rc.1` - Beta tags are published GitHub prereleases like `v0.1.41-beta.1`
- RCs publish desktop assets and APKs for testing, but they do not publish npm packages and do not trigger the production web/mobile release flows - Betas publish desktop assets and APKs for testing, but they do not publish npm packages and do not trigger the production web/mobile release flows
- `release:promote` creates a fresh stable tag like `v0.1.41`; the final release never reuses the RC tag - `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` - Desktop assets now come from the Electron package at `packages/desktop`
- **Do NOT create a changelog entry for RCs.** The changelog remains stable-only. RC release notes are generated automatically so the website stays pinned to the latest published stable release. - 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.
- **Do create a changelog entry for betas.** The beta entry is temporary and gets updated in place until promotion.
Use the RC path when you need to: Use the beta path when you need to:
- test a build manually in a Linux or Windows VM - test a build manually in a Linux or Windows VM
- send a build to a user who is hitting a specific problem - send a build to a user who is hitting a specific problem
- iterate on `rc.1`, `rc.2`, `rc.3`, and so on before deciding to ship broadly - iterate on `beta.1`, `beta.2`, `beta.3`, and so on before deciding to ship broadly
## Website behavior ## Website behavior
- The website download page points to GitHub's latest published **stable** release. - The website download page points to GitHub's latest published **stable** release.
- Published RC prereleases are public on GitHub Releases, but they do **not** become the website download target. - Published beta prereleases are public on GitHub Releases, but they do **not** become the website download target.
- The website only moves when you publish the final stable release tag like `v0.1.41`. - The website only moves when you publish the final stable release tag like `v0.1.41`.
## Fixing a failed release build ## Fixing a failed release build
@@ -88,13 +89,13 @@ git tag -f desktop-windows-v0.1.28 HEAD && git push origin desktop-windows-v0.1.
# Android APK # Android APK
git tag -f android-v0.1.28 HEAD && git push origin android-v0.1.28 --force git tag -f android-v0.1.28 HEAD && git push origin android-v0.1.28 --force
# RC # Beta
git tag -f v0.1.29-rc.2 HEAD && git push origin v0.1.29-rc.2 --force 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. This ensures the checkout ref matches the actual code on `main` with the fix included.
- `vX.Y.Z` or `vX.Y.Z-rc.N` rebuilds the full tagged release - `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-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 - `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 - `android-vX.Y.Z` rebuilds the Android APK release only
@@ -105,24 +106,26 @@ This ensures the checkout ref matches the actual code on `main` with the fix inc
- `release:prepare` refreshes workspace `node_modules` links to prevent stale types - `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` - `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` partially fails, re-run it — npm skips already-published versions
- The website uses GitHub's latest published release API for download links, so published RC prereleases do not replace the stable download target. - 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 ## Changelog format
Stable release notes depend on the changelog heading format. The heading **must** be strictly followed: Release notes depend on the changelog heading format. The heading **must** be strictly followed:
``` ```
## X.Y.Z - YYYY-MM-DD ## X.Y.Z - YYYY-MM-DD
## X.Y.Z-beta.N - YYYY-MM-DD
``` ```
No prefix (`v`), no extra text. The parser matches the first `## X.Y.Z` line to extract the version. A malformed heading will break download links on the homepage. No prefix (`v`), no extra text. The parser matches the first `## X.Y.Z` line to extract the version. A malformed heading will break download links on the homepage.
## Changelog policy ## Changelog policy
- `CHANGELOG.md` is for **final stable releases only**. - `CHANGELOG.md` includes stable releases and the current beta line.
- Do not add or edit changelog entries while iterating on RCs. - The first beta inserts a top entry like `## 0.1.60-beta.1 - YYYY-MM-DD`.
- Write the proper changelog entry when you are cutting the final stable release that comes after the RC cycle. - The next beta updates that same top entry in place, for example from `0.1.60-beta.1` to `0.1.60-beta.2`.
- Between stable releases, keep changelog work out of the repo until the final release is ready. - Stable promotion updates that same entry in place, for example from `0.1.60-beta.2` to `0.1.60`.
- Do not create duplicate entries for each beta on the same version line.
## Changelog ownership ## Changelog ownership
@@ -178,7 +181,7 @@ Entries within each section (Added, Improved, Fixed) are ordered by user impact:
## Pre-release sanity check ## Pre-release sanity check
Before cutting any release (RC or stable), run a Codex review of the diff as a last line of defence against shipping bugs. Before cutting any release (beta or stable), run a Codex review of the diff as a last line of defence against shipping bugs.
Load the `paseo` skill and launch a **Codex 5.4** agent with a prompt like: Load the `paseo` skill and launch a **Codex 5.4** agent with a prompt like:
@@ -196,10 +199,10 @@ The agent's job is a deep sanity check, not a full code review. If it flags anyt
The changelog always covers **stable-to-HEAD**: The changelog always covers **stable-to-HEAD**:
- **RC release**: the diff and release notes cover `latest stable tag HEAD`. RC release notes are auto-generated and not added to `CHANGELOG.md`. - **Beta release**: the diff and release notes cover `latest stable tag -> HEAD`. The current beta changelog entry is updated in place.
- **Stable release**: the diff and changelog entry cover `latest stable tag → HEAD`. Any intermediate RCs are skipped — the changelog captures the full delta from the previous stable release, not just what changed since the last RC. - **Stable release**: the same changelog entry is promoted in place. It still captures the full delta from the previous stable release, not just what changed since the last beta.
In other words, RCs are checkpoints along the way; the changelog only records the final jump from one stable version to the next. In other words, betas are checkpoints along the way; the changelog entry remains the single record for the final jump from one stable version to the next.
## Completion checklist ## Completion checklist

38
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "paseo", "name": "paseo",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "paseo", "name": "paseo",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"workspaces": [ "workspaces": [
@@ -37303,16 +37303,16 @@
}, },
"packages/app": { "packages/app": {
"name": "@getpaseo/app", "name": "@getpaseo/app",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@expo/vector-icons": "^15.0.2", "@expo/vector-icons": "^15.0.2",
"@floating-ui/react-native": "^0.10.7", "@floating-ui/react-native": "^0.10.7",
"@getpaseo/expo-two-way-audio": "0.1.60-rc.3", "@getpaseo/expo-two-way-audio": "0.1.60-beta.1",
"@getpaseo/highlight": "0.1.60-rc.3", "@getpaseo/highlight": "0.1.60-beta.1",
"@getpaseo/server": "0.1.60-rc.3", "@getpaseo/server": "0.1.60-beta.1",
"@gorhom/bottom-sheet": "^5.2.6", "@gorhom/bottom-sheet": "^5.2.6",
"@gorhom/portal": "^1.0.14", "@gorhom/portal": "^1.0.14",
"@react-native-async-storage/async-storage": "2.2.0", "@react-native-async-storage/async-storage": "2.2.0",
@@ -37456,11 +37456,11 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@getpaseo/cli", "name": "@getpaseo/cli",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"dependencies": { "dependencies": {
"@clack/prompts": "^1.0.0", "@clack/prompts": "^1.0.0",
"@getpaseo/relay": "0.1.60-rc.3", "@getpaseo/relay": "0.1.60-beta.1",
"@getpaseo/server": "0.1.60-rc.3", "@getpaseo/server": "0.1.60-beta.1",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"commander": "^12.0.0", "commander": "^12.0.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
@@ -37501,11 +37501,11 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@getpaseo/desktop", "name": "@getpaseo/desktop",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@getpaseo/cli": "0.1.60-rc.3", "@getpaseo/cli": "0.1.60-beta.1",
"@getpaseo/server": "0.1.60-rc.3", "@getpaseo/server": "0.1.60-beta.1",
"electron-log": "^5.4.3", "electron-log": "^5.4.3",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"ws": "^8.14.2" "ws": "^8.14.2"
@@ -37539,7 +37539,7 @@
}, },
"packages/expo-two-way-audio": { "packages/expo-two-way-audio": {
"name": "@getpaseo/expo-two-way-audio", "name": "@getpaseo/expo-two-way-audio",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
@@ -37740,7 +37740,7 @@
}, },
"packages/highlight": { "packages/highlight": {
"name": "@getpaseo/highlight", "name": "@getpaseo/highlight",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"dependencies": { "dependencies": {
"@lezer/common": "^1.5.0", "@lezer/common": "^1.5.0",
"@lezer/cpp": "^1.1.5", "@lezer/cpp": "^1.1.5",
@@ -37766,7 +37766,7 @@
}, },
"packages/relay": { "packages/relay": {
"name": "@getpaseo/relay", "name": "@getpaseo/relay",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"dependencies": { "dependencies": {
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
@@ -37782,14 +37782,14 @@
}, },
"packages/server": { "packages/server": {
"name": "@getpaseo/server", "name": "@getpaseo/server",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"dependencies": { "dependencies": {
"@agentclientprotocol/sdk": "^0.17.1", "@agentclientprotocol/sdk": "^0.17.1",
"@ai-sdk/openai": "2.0.52", "@ai-sdk/openai": "2.0.52",
"@anthropic-ai/claude-agent-sdk": "^0.2.11", "@anthropic-ai/claude-agent-sdk": "^0.2.11",
"@deepgram/sdk": "^3.4.0", "@deepgram/sdk": "^3.4.0",
"@getpaseo/highlight": "0.1.60-rc.3", "@getpaseo/highlight": "0.1.60-beta.1",
"@getpaseo/relay": "0.1.60-rc.3", "@getpaseo/relay": "0.1.60-beta.1",
"@isaacs/ttlcache": "^2.1.4", "@isaacs/ttlcache": "^2.1.4",
"@mariozechner/pi-coding-agent": "^0.67.68", "@mariozechner/pi-coding-agent": "^0.67.68",
"@modelcontextprotocol/sdk": "^1.20.1", "@modelcontextprotocol/sdk": "^1.20.1",
@@ -38543,7 +38543,7 @@
}, },
"packages/website": { "packages/website": {
"name": "@getpaseo/website", "name": "@getpaseo/website",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "^1.20.3", "@cloudflare/vite-plugin": "^1.20.3",
"@cloudflare/workers-types": "^4.20260114.0", "@cloudflare/workers-types": "^4.20260114.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "paseo", "name": "paseo",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"packages/expo-two-way-audio", "packages/expo-two-way-audio",
@@ -47,19 +47,19 @@
"version:all:patch": "node scripts/set-release-version.mjs --mode patch", "version:all:patch": "node scripts/set-release-version.mjs --mode patch",
"version:all:minor": "node scripts/set-release-version.mjs --mode minor", "version:all:minor": "node scripts/set-release-version.mjs --mode minor",
"version:all:major": "node scripts/set-release-version.mjs --mode major", "version:all:major": "node scripts/set-release-version.mjs --mode major",
"version:all:rc:patch": "node scripts/set-release-version.mjs --mode rc-patch", "version:all:beta:patch": "node scripts/set-release-version.mjs --mode beta-patch",
"version:all:rc:minor": "node scripts/set-release-version.mjs --mode rc-minor", "version:all:beta:minor": "node scripts/set-release-version.mjs --mode beta-minor",
"version:all:rc:major": "node scripts/set-release-version.mjs --mode rc-major", "version:all:beta:major": "node scripts/set-release-version.mjs --mode beta-major",
"version:all:rc:next": "node scripts/set-release-version.mjs --mode rc-next", "version:all:beta:next": "node scripts/set-release-version.mjs --mode beta-next",
"version:all:promote": "node scripts/set-release-version.mjs --mode promote", "version:all:promote": "node scripts/set-release-version.mjs --mode promote",
"release:check": "npm run release:prepare && npm run typecheck --workspace=@getpaseo/highlight && npm run build --workspace=@getpaseo/highlight && npm run typecheck --workspace=@getpaseo/relay && npm run build --workspace=@getpaseo/relay && npm run typecheck --workspace=@getpaseo/server && npm run build --workspace=@getpaseo/server && npm run typecheck --workspace=@getpaseo/cli && npm run build --workspace=@getpaseo/cli && npm pack --dry-run --workspace=@getpaseo/highlight && npm pack --dry-run --workspace=@getpaseo/relay && npm pack --dry-run --workspace=@getpaseo/server && npm pack --dry-run --workspace=@getpaseo/cli", "release:check": "npm run release:prepare && npm run typecheck --workspace=@getpaseo/highlight && npm run build --workspace=@getpaseo/highlight && npm run typecheck --workspace=@getpaseo/relay && npm run build --workspace=@getpaseo/relay && npm run typecheck --workspace=@getpaseo/server && npm run build --workspace=@getpaseo/server && npm run typecheck --workspace=@getpaseo/cli && npm run build --workspace=@getpaseo/cli && npm pack --dry-run --workspace=@getpaseo/highlight && npm pack --dry-run --workspace=@getpaseo/relay && npm pack --dry-run --workspace=@getpaseo/server && npm pack --dry-run --workspace=@getpaseo/cli",
"release:publish:dry-run": "npm publish --dry-run --workspace=@getpaseo/highlight --access public && npm publish --dry-run --workspace=@getpaseo/relay --access public && npm publish --dry-run --workspace=@getpaseo/server --access public && npm publish --dry-run --workspace=@getpaseo/cli --access public", "release:publish:dry-run": "npm publish --dry-run --workspace=@getpaseo/highlight --access public && npm publish --dry-run --workspace=@getpaseo/relay --access public && npm publish --dry-run --workspace=@getpaseo/server --access public && npm publish --dry-run --workspace=@getpaseo/cli --access public",
"release:publish": "npm publish --workspace=@getpaseo/highlight --access public && npm publish --workspace=@getpaseo/relay --access public && npm publish --workspace=@getpaseo/server --access public && npm publish --workspace=@getpaseo/cli --access public", "release:publish": "npm publish --workspace=@getpaseo/highlight --access public && npm publish --workspace=@getpaseo/relay --access public && npm publish --workspace=@getpaseo/server --access public && npm publish --workspace=@getpaseo/cli --access public",
"release:push": "node scripts/push-current-release-tag.mjs", "release:push": "node scripts/push-current-release-tag.mjs",
"release:rc:patch": "npm run release:check && npm run version:all:rc:patch && npm run release:push", "release:beta:patch": "npm run release:check && npm run version:all:beta:patch && npm run release:push",
"release:rc:minor": "npm run release:check && npm run version:all:rc:minor && npm run release:push", "release:beta:minor": "npm run release:check && npm run version:all:beta:minor && npm run release:push",
"release:rc:major": "npm run release:check && npm run version:all:rc:major && npm run release:push", "release:beta:major": "npm run release:check && npm run version:all:beta:major && npm run release:push",
"release:rc:next": "npm run release:check && npm run version:all:rc:next && npm run release:push", "release:beta:next": "npm run release:check && npm run version:all:beta:next && npm run release:push",
"release:promote": "npm run release:check && npm run version:all:promote && npm run release:publish && npm run release:push", "release:promote": "npm run release:check && npm run version:all:promote && npm run release:publish && npm run release:push",
"release:patch": "npm run release:check && npm run version:all:patch && npm run release:publish && npm run release:push", "release:patch": "npm run release:check && npm run version:all:patch && npm run release:publish && npm run release:push",
"release:minor": "npm run release:check && npm run version:all:minor && npm run release:publish && npm run release:push", "release:minor": "npm run release:check && npm run version:all:minor && npm run release:publish && npm run release:push",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@getpaseo/app", "name": "@getpaseo/app",
"main": "index.ts", "main": "index.ts",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "APP_VARIANT=development expo start", "start": "APP_VARIANT=development expo start",
@@ -31,9 +31,9 @@
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@expo/vector-icons": "^15.0.2", "@expo/vector-icons": "^15.0.2",
"@floating-ui/react-native": "^0.10.7", "@floating-ui/react-native": "^0.10.7",
"@getpaseo/expo-two-way-audio": "0.1.60-rc.3", "@getpaseo/expo-two-way-audio": "0.1.60-beta.1",
"@getpaseo/highlight": "0.1.60-rc.3", "@getpaseo/highlight": "0.1.60-beta.1",
"@getpaseo/server": "0.1.60-rc.3", "@getpaseo/server": "0.1.60-beta.1",
"@gorhom/bottom-sheet": "^5.2.6", "@gorhom/bottom-sheet": "^5.2.6",
"@gorhom/portal": "^1.0.14", "@gorhom/portal": "^1.0.14",
"@react-native-async-storage/async-storage": "2.2.0", "@react-native-async-storage/async-storage": "2.2.0",

View File

@@ -35,12 +35,13 @@ export function SegmentedControl<T extends string>({
testID, testID,
}: SegmentedControlProps<T>) { }: SegmentedControlProps<T>) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const containerSizeStyle = size === "sm" ? styles.containerSm : styles.containerMd;
const segmentSizeStyle = size === "sm" ? styles.segmentSm : styles.segmentMd; const segmentSizeStyle = size === "sm" ? styles.segmentSm : styles.segmentMd;
const labelSizeStyle = size === "sm" ? styles.labelSm : styles.labelMd; const labelSizeStyle = size === "sm" ? styles.labelSm : styles.labelMd;
const iconSize = size === "sm" ? theme.iconSize.sm : theme.iconSize.md; const iconSize = size === "sm" ? theme.iconSize.sm : theme.iconSize.md;
return ( return (
<View style={[styles.container, style]} testID={testID}> <View style={[styles.container, containerSizeStyle, style]} testID={testID}>
{options.map((option) => { {options.map((option) => {
const isSelected = option.value === value; const isSelected = option.value === value;
const iconColor = isSelected ? theme.colors.foreground : theme.colors.foregroundMuted; const iconColor = isSelected ? theme.colors.foreground : theme.colors.foregroundMuted;
@@ -89,39 +90,46 @@ export function SegmentedControl<T extends string>({
const styles = StyleSheet.create((theme) => ({ const styles = StyleSheet.create((theme) => ({
container: { container: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "stretch",
maxWidth: "100%", maxWidth: "100%",
gap: 4, backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
},
containerSm: {
padding: 2,
},
containerMd: {
padding: 3,
}, },
segment: { segment: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexShrink: 0, flexShrink: 0,
borderRadius: theme.borderRadius.full, borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: "transparent",
gap: theme.spacing[1], gap: theme.spacing[1],
}, },
segmentSm: { segmentSm: {
paddingVertical: theme.spacing[1], paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[2], paddingHorizontal: theme.spacing[4],
}, },
segmentMd: { segmentMd: {
paddingVertical: theme.spacing[2], paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[3], paddingHorizontal: theme.spacing[6],
}, },
segmentSelected: { segmentSelected: {
backgroundColor: theme.colors.surface3, backgroundColor: theme.colors.surface0,
borderColor: theme.colors.border, shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
elevation: 1,
}, },
segmentHover: { segmentHover: {
backgroundColor: theme.colors.surface3, backgroundColor: theme.colors.surface1,
borderColor: theme.colors.borderAccent,
}, },
segmentPressed: { segmentPressed: {
backgroundColor: theme.colors.surface3, backgroundColor: theme.colors.surface1,
borderColor: theme.colors.borderAccent,
}, },
segmentDisabled: { segmentDisabled: {
opacity: theme.opacity[50], opacity: theme.opacity[50],

View File

@@ -17,6 +17,8 @@ export interface DesktopAppUpdateInstallResult {
message: string; message: string;
} }
export type DesktopReleaseChannel = "stable" | "beta";
export interface LocalDaemonUpdateResult { export interface LocalDaemonUpdateResult {
exitCode: number; exitCode: number;
stdout: string; stdout: string;
@@ -69,8 +71,12 @@ export async function getLocalDaemonVersion(): Promise<LocalDaemonVersionResult>
return parseLocalDaemonVersionResult(result); return parseLocalDaemonVersionResult(result);
} }
export async function checkDesktopAppUpdate(): Promise<DesktopAppUpdateCheckResult> { export async function checkDesktopAppUpdate({
const result = await invokeDesktopCommand<unknown>("check_app_update"); releaseChannel,
}: {
releaseChannel: DesktopReleaseChannel;
}): Promise<DesktopAppUpdateCheckResult> {
const result = await invokeDesktopCommand<unknown>("check_app_update", { releaseChannel });
if (!isRecord(result)) { if (!isRecord(result)) {
throw new Error("Unexpected response while checking desktop updates."); throw new Error("Unexpected response while checking desktop updates.");
} }
@@ -85,8 +91,12 @@ export async function checkDesktopAppUpdate(): Promise<DesktopAppUpdateCheckResu
}; };
} }
export async function installDesktopAppUpdate(): Promise<DesktopAppUpdateInstallResult> { export async function installDesktopAppUpdate({
const result = await invokeDesktopCommand<unknown>("install_app_update"); releaseChannel,
}: {
releaseChannel: DesktopReleaseChannel;
}): Promise<DesktopAppUpdateInstallResult> {
const result = await invokeDesktopCommand<unknown>("install_app_update", { releaseChannel });
if (!isRecord(result)) { if (!isRecord(result)) {
throw new Error("Unexpected response while installing desktop update."); throw new Error("Unexpected response while installing desktop update.");
} }

View File

@@ -7,6 +7,7 @@ import {
type DesktopAppUpdateCheckResult, type DesktopAppUpdateCheckResult,
type DesktopAppUpdateInstallResult, type DesktopAppUpdateInstallResult,
} from "@/desktop/updates/desktop-updates"; } from "@/desktop/updates/desktop-updates";
import { useAppSettings } from "@/hooks/use-settings";
export type DesktopAppUpdateStatus = export type DesktopAppUpdateStatus =
| "idle" | "idle"
@@ -83,6 +84,8 @@ function formatStatusText(input: {
export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn { export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
const isDesktopApp = shouldShowDesktopUpdateSection(); const isDesktopApp = shouldShowDesktopUpdateSection();
const { settings } = useAppSettings();
const releaseChannel = settings.releaseChannel;
const requestVersionRef = useRef(0); const requestVersionRef = useRef(0);
const [status, setStatus] = useState<DesktopAppUpdateStatus>("idle"); const [status, setStatus] = useState<DesktopAppUpdateStatus>("idle");
const [availableUpdate, setAvailableUpdate] = useState<DesktopAppUpdateCheckResult | null>(null); const [availableUpdate, setAvailableUpdate] = useState<DesktopAppUpdateCheckResult | null>(null);
@@ -105,7 +108,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
setErrorMessage(null); setErrorMessage(null);
try { try {
const result = await checkDesktopAppUpdate(); const result = await checkDesktopAppUpdate({ releaseChannel });
if (requestVersion !== requestVersionRef.current) { if (requestVersion !== requestVersionRef.current) {
return result; return result;
} }
@@ -140,9 +143,17 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
return null; return null;
} }
}, },
[isDesktopApp], [isDesktopApp, releaseChannel],
); );
useEffect(() => {
if (!isDesktopApp) {
return;
}
void checkForUpdates({ silent: true });
}, [checkForUpdates, isDesktopApp]);
useEffect(() => { useEffect(() => {
if (!isDesktopApp || status !== "pending") { if (!isDesktopApp || status !== "pending") {
return undefined; return undefined;
@@ -166,7 +177,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
setErrorMessage(null); setErrorMessage(null);
try { try {
const result = await installDesktopAppUpdate(); const result = await installDesktopAppUpdate({ releaseChannel });
setLastCheckedAt(Date.now()); setLastCheckedAt(Date.now());
if (result.installed) { if (result.installed) {
@@ -186,7 +197,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
setErrorMessage(message); setErrorMessage(message);
return null; return null;
} }
}, [isDesktopApp]); }, [isDesktopApp, releaseChannel]);
return { return {
isDesktopApp, isDesktopApp,

View File

@@ -40,6 +40,16 @@ describe("use-settings", () => {
expect(result.theme).toBe("auto"); expect(result.theme).toBe("auto");
}); });
it("defaults release channel to stable when storage is empty", async () => {
asyncStorageMock.getItem.mockResolvedValue(null);
asyncStorageMock.setItem.mockResolvedValue();
const mod = await import("./use-settings");
const result = await mod.loadSettingsFromStorage();
expect(result.releaseChannel).toBe("stable");
});
it("loads persisted built-in daemon management state", async () => { it("loads persisted built-in daemon management state", async () => {
asyncStorageMock.getItem.mockImplementation(async (key: string) => { asyncStorageMock.getItem.mockImplementation(async (key: string) => {
if (key === "@paseo:app-settings") { if (key === "@paseo:app-settings") {
@@ -58,7 +68,24 @@ describe("use-settings", () => {
theme: "light", theme: "light",
manageBuiltInDaemon: false, manageBuiltInDaemon: false,
sendBehavior: "interrupt", sendBehavior: "interrupt",
releaseChannel: "stable",
}); });
expect(asyncStorageMock.setItem).not.toHaveBeenCalled(); expect(asyncStorageMock.setItem).not.toHaveBeenCalled();
}); });
it("loads persisted beta release channel", async () => {
asyncStorageMock.getItem.mockImplementation(async (key: string) => {
if (key === "@paseo:app-settings") {
return JSON.stringify({
releaseChannel: "beta",
});
}
return null;
});
const mod = await import("./use-settings");
const result = await mod.loadSettingsFromStorage();
expect(result.releaseChannel).toBe("beta");
});
}); });

View File

@@ -9,19 +9,23 @@ const APP_SETTINGS_QUERY_KEY = ["app-settings"];
import { THEME_TO_UNISTYLES, type ThemeName } from "@/styles/theme"; import { THEME_TO_UNISTYLES, type ThemeName } from "@/styles/theme";
export type SendBehavior = "interrupt" | "queue"; export type SendBehavior = "interrupt" | "queue";
export type ReleaseChannel = "stable" | "beta";
const VALID_THEMES = new Set<string>([...Object.keys(THEME_TO_UNISTYLES), "auto"]); const VALID_THEMES = new Set<string>([...Object.keys(THEME_TO_UNISTYLES), "auto"]);
const VALID_RELEASE_CHANNELS = new Set<string>(["stable", "beta"]);
export interface AppSettings { export interface AppSettings {
theme: ThemeName | "auto"; theme: ThemeName | "auto";
manageBuiltInDaemon: boolean; manageBuiltInDaemon: boolean;
sendBehavior: SendBehavior; sendBehavior: SendBehavior;
releaseChannel: ReleaseChannel;
} }
export const DEFAULT_APP_SETTINGS: AppSettings = { export const DEFAULT_APP_SETTINGS: AppSettings = {
theme: "auto", theme: "auto",
manageBuiltInDaemon: true, manageBuiltInDaemon: true,
sendBehavior: "interrupt", sendBehavior: "interrupt",
releaseChannel: "stable",
}; };
export interface UseAppSettingsReturn { export interface UseAppSettingsReturn {
@@ -85,6 +89,9 @@ export async function loadSettingsFromStorage(): Promise<AppSettings> {
if (parsed.theme && !VALID_THEMES.has(parsed.theme)) { if (parsed.theme && !VALID_THEMES.has(parsed.theme)) {
parsed.theme = DEFAULT_APP_SETTINGS.theme; parsed.theme = DEFAULT_APP_SETTINGS.theme;
} }
if (parsed.releaseChannel && !VALID_RELEASE_CHANNELS.has(parsed.releaseChannel)) {
parsed.releaseChannel = DEFAULT_APP_SETTINGS.releaseChannel;
}
return { ...DEFAULT_APP_SETTINGS, ...parsed }; return { ...DEFAULT_APP_SETTINGS, ...parsed };
} }
@@ -115,6 +122,9 @@ function pickAppSettingsFromLegacy(legacy: Record<string, unknown>): Partial<App
if (typeof legacy.manageBuiltInDaemon === "boolean") { if (typeof legacy.manageBuiltInDaemon === "boolean") {
result.manageBuiltInDaemon = legacy.manageBuiltInDaemon; result.manageBuiltInDaemon = legacy.manageBuiltInDaemon;
} }
if (legacy.releaseChannel === "stable" || legacy.releaseChannel === "beta") {
result.releaseChannel = legacy.releaseChannel;
}
return result; return result;
} }

View File

@@ -281,6 +281,7 @@ function AboutSection({ appVersionText, isDesktopApp }: AboutSectionProps) {
} }
function DesktopAppUpdateRow() { function DesktopAppUpdateRow() {
const { settings, updateSettings } = useAppSettings();
const { const {
isDesktopApp, isDesktopApp,
statusText, statusText,
@@ -309,6 +310,13 @@ function DesktopAppUpdateRow() {
void checkForUpdates(); void checkForUpdates();
}, [checkForUpdates, isDesktopApp]); }, [checkForUpdates, isDesktopApp]);
const handleReleaseChannelChange = useCallback(
(releaseChannel: AppSettings["releaseChannel"]) => {
void updateSettings({ releaseChannel });
},
[updateSettings],
);
const handleInstallUpdate = useCallback(() => { const handleInstallUpdate = useCallback(() => {
if (!isDesktopApp) { if (!isDesktopApp) {
return; return;
@@ -337,40 +345,59 @@ function DesktopAppUpdateRow() {
} }
return ( return (
<View style={[settingsStyles.row, settingsStyles.rowBorder]}> <>
<View style={settingsStyles.rowContent}> <View style={[settingsStyles.row, settingsStyles.rowBorder]}>
<Text style={settingsStyles.rowTitle}>App updates</Text> <View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowHint}>{statusText}</Text> <Text style={settingsStyles.rowTitle}>Release channel</Text>
{availableUpdate?.latestVersion ? (
<Text style={settingsStyles.rowHint}> <Text style={settingsStyles.rowHint}>
Ready to install: {formatVersionWithPrefix(availableUpdate.latestVersion)} Switch to Beta to get updates sooner and help shape them.
</Text> </Text>
) : null} </View>
{errorMessage ? <Text style={styles.aboutErrorText}>{errorMessage}</Text> : null} <SegmentedControl
</View>
<View style={styles.aboutUpdateActions}>
<Button
variant="outline"
size="sm" size="sm"
onPress={handleCheckForUpdates} value={settings.releaseChannel}
disabled={isChecking || isInstalling} onValueChange={handleReleaseChannelChange}
> options={[
{isChecking ? "Checking..." : "Check"} { value: "stable", label: "Stable" },
</Button> { value: "beta", label: "Beta" },
<Button ]}
variant="default" />
size="sm"
onPress={handleInstallUpdate}
disabled={isChecking || isInstalling || !availableUpdate}
>
{isInstalling
? "Installing..."
: availableUpdate?.latestVersion
? `Update to ${formatVersionWithPrefix(availableUpdate.latestVersion)}`
: "Update"}
</Button>
</View> </View>
</View> <View style={[settingsStyles.row, settingsStyles.rowBorder]}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>App updates</Text>
<Text style={settingsStyles.rowHint}>{statusText}</Text>
{availableUpdate?.latestVersion ? (
<Text style={settingsStyles.rowHint}>
Ready to install: {formatVersionWithPrefix(availableUpdate.latestVersion)}
</Text>
) : null}
{errorMessage ? <Text style={styles.aboutErrorText}>{errorMessage}</Text> : null}
</View>
<View style={styles.aboutUpdateActions}>
<Button
variant="outline"
size="sm"
onPress={handleCheckForUpdates}
disabled={isChecking || isInstalling}
>
{isChecking ? "Checking..." : "Check"}
</Button>
<Button
variant="default"
size="sm"
onPress={handleInstallUpdate}
disabled={isChecking || isInstalling || !availableUpdate}
>
{isInstalling
? "Installing..."
: availableUpdate?.latestVersion
? `Update to ${formatVersionWithPrefix(availableUpdate.latestVersion)}`
: "Update"}
</Button>
</View>
</View>
</>
); );
} }
@@ -858,9 +885,10 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
layout="desktop" layout="desktop"
/> />
<View style={desktopStyles.contentPane}> <View style={desktopStyles.contentPane}>
{detailHeader ? ( <ScreenHeader
<ScreenHeader borderless={!detailHeader}
left={ left={
detailHeader ? (
<> <>
<HeaderIconBadge> <HeaderIconBadge>
<detailHeader.Icon <detailHeader.Icon
@@ -873,10 +901,10 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
</ScreenTitle> </ScreenTitle>
{detailHeader.titleAccessory} {detailHeader.titleAccessory}
</> </>
} ) : null
leftStyle={desktopStyles.detailLeft} }
/> leftStyle={desktopStyles.detailLeft}
) : null} />
<ScrollView <ScrollView
style={styles.scrollView} style={styles.scrollView}
contentContainerStyle={{ paddingBottom: insets.bottom }} contentContainerStyle={{ paddingBottom: insets.bottom }}

View File

@@ -19,58 +19,66 @@ type RawWindowControlsPadding = {
type WindowControlsPaddingRole = "sidebar" | "header" | "tabRow" | "explorerSidebar"; type WindowControlsPaddingRole = "sidebar" | "header" | "tabRow" | "explorerSidebar";
// Module-level cache so hook remounts (e.g., on navigation) don't briefly
// fall back to the default `false` while the async fullscreen check resolves.
// Without this, in fullscreen the sidebar flashes with traffic-light padding
// on first frame and then snaps to 0 once the async read completes.
let cachedIsFullscreen = false;
const fullscreenSubscribers = new Set<(value: boolean) => void>();
let fullscreenSubscriptionStarted = false;
function setCachedFullscreen(value: boolean) {
if (cachedIsFullscreen === value) return;
cachedIsFullscreen = value;
for (const sub of fullscreenSubscribers) {
sub(value);
}
}
function startFullscreenSubscription() {
if (fullscreenSubscriptionStarted) return;
if (isNative || !getIsElectronRuntime()) return;
fullscreenSubscriptionStarted = true;
void (async () => {
const win = getDesktopWindow();
if (!win) return;
if (typeof win.isFullscreen === "function") {
try {
setCachedFullscreen(await win.isFullscreen());
} catch (error) {
console.warn("[DesktopWindow] Failed to read fullscreen state", error);
}
}
if (typeof win.onResized !== "function") return;
try {
await win.onResized(async () => {
if (typeof win.isFullscreen !== "function") return;
try {
setCachedFullscreen(await win.isFullscreen());
} catch (error) {
console.warn("[DesktopWindow] Failed to read fullscreen state", error);
}
});
} catch (error) {
console.warn("[DesktopWindow] Failed to subscribe to resize", error);
}
})();
}
function useRawWindowControlsPadding(): RawWindowControlsPadding { function useRawWindowControlsPadding(): RawWindowControlsPadding {
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(cachedIsFullscreen);
useEffect(() => { useEffect(() => {
if (isNative || !getIsElectronRuntime()) return; startFullscreenSubscription();
// Sync to any value that resolved between render and effect.
let disposed = false; setIsFullscreen(cachedIsFullscreen);
let cleanup: (() => void) | undefined; fullscreenSubscribers.add(setIsFullscreen);
let didCleanup = false;
function runCleanup() {
if (!cleanup || didCleanup) return;
didCleanup = true;
try {
void Promise.resolve(cleanup()).catch((error) => {
console.warn("[DesktopWindow] Failed to remove resize listener", error);
});
} catch (error) {
console.warn("[DesktopWindow] Failed to remove resize listener", error);
}
}
async function setup() {
const win = getDesktopWindow();
if (!win) return;
const fullscreen = typeof win.isFullscreen === "function" ? await win.isFullscreen() : false;
if (disposed) return;
setIsFullscreen(fullscreen);
if (typeof win.onResized !== "function") {
return;
}
const unlisten = await win.onResized(async () => {
if (disposed) return;
const fs = typeof win.isFullscreen === "function" ? await win.isFullscreen() : false;
if (disposed) return;
setIsFullscreen(fs);
});
cleanup = unlisten;
if (disposed) {
runCleanup();
}
}
void setup();
return () => { return () => {
disposed = true; fullscreenSubscribers.delete(setIsFullscreen);
runCleanup();
}; };
}, []); }, []);

View File

@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/cli", "name": "@getpaseo/cli",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"description": "Paseo CLI - control your AI coding agents from the command line", "description": "Paseo CLI - control your AI coding agents from the command line",
"type": "module", "type": "module",
"files": [ "files": [
@@ -24,8 +24,8 @@
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "^1.0.0", "@clack/prompts": "^1.0.0",
"@getpaseo/relay": "0.1.60-rc.3", "@getpaseo/relay": "0.1.60-beta.1",
"@getpaseo/server": "0.1.60-rc.3", "@getpaseo/server": "0.1.60-beta.1",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"commander": "^12.0.0", "commander": "^12.0.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/desktop", "name": "@getpaseo/desktop",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"private": true, "private": true,
"description": "Paseo desktop app (Electron wrapper)", "description": "Paseo desktop app (Electron wrapper)",
"main": "dist/main.js", "main": "dist/main.js",
@@ -13,8 +13,8 @@
"typecheck": "tsc --noEmit -p tsconfig.json" "typecheck": "tsc --noEmit -p tsconfig.json"
}, },
"dependencies": { "dependencies": {
"@getpaseo/cli": "0.1.60-rc.3", "@getpaseo/cli": "0.1.60-beta.1",
"@getpaseo/server": "0.1.60-rc.3", "@getpaseo/server": "0.1.60-beta.1",
"electron-log": "^5.4.3", "electron-log": "^5.4.3",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"ws": "^8.14.2" "ws": "^8.14.2"

View File

@@ -11,7 +11,11 @@ import {
readManagedFileBase64, readManagedFileBase64,
writeAttachmentBase64, writeAttachmentBase64,
} from "../features/attachments.js"; } from "../features/attachments.js";
import { checkForAppUpdate, downloadAndInstallUpdate } from "../features/auto-updater.js"; import {
checkForAppUpdate,
downloadAndInstallUpdate,
type AppReleaseChannel,
} from "../features/auto-updater.js";
import { import {
installCli, installCli,
getCliInstallStatus, getCliInstallStatus,
@@ -65,6 +69,10 @@ type DesktopPairingOffer = {
type DesktopCommandHandler = (args?: Record<string, unknown>) => Promise<unknown> | unknown; type DesktopCommandHandler = (args?: Record<string, unknown>) => Promise<unknown> | unknown;
function parseReleaseChannel(args: Record<string, unknown> | undefined): AppReleaseChannel {
return args?.releaseChannel === "beta" ? "beta" : "stable";
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Utilities // Utilities
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -483,15 +491,18 @@ export function createDaemonCommandHandlers(): Record<string, DesktopCommandHand
: ""; : "";
if (sessionId) closeLocalTransportSession(sessionId); if (sessionId) closeLocalTransportSession(sessionId);
}, },
check_app_update: async () => { check_app_update: async (args) => {
const currentVersion = await resolveCurrentUpdateVersion(); const currentVersion = await resolveCurrentUpdateVersion();
return checkForAppUpdate(currentVersion); return checkForAppUpdate({ currentVersion, releaseChannel: parseReleaseChannel(args) });
}, },
install_app_update: async () => { install_app_update: async (args) => {
const currentVersion = await resolveCurrentUpdateVersion(); const currentVersion = await resolveCurrentUpdateVersion();
return downloadAndInstallUpdate(currentVersion, async () => { return downloadAndInstallUpdate(
await stopDaemon(); { currentVersion, releaseChannel: parseReleaseChannel(args) },
}); async () => {
await stopDaemon();
},
);
}, },
get_local_daemon_version: () => getLocalDaemonVersion(), get_local_daemon_version: () => getLocalDaemonVersion(),
install_cli: () => installCli(), install_cli: () => installCli(),

View File

@@ -20,6 +20,8 @@ export type AppUpdateInstallResult = {
message: string; message: string;
}; };
export type AppReleaseChannel = "stable" | "beta";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// State // State
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -28,18 +30,29 @@ let cachedUpdateInfo: UpdateInfo | null = null;
let downloadedUpdateVersion: string | null = null; let downloadedUpdateVersion: string | null = null;
let downloading = false; let downloading = false;
let autoUpdaterConfigured = false; let autoUpdaterConfigured = false;
let configuredReleaseChannel: AppReleaseChannel | null = null;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Configuration // Configuration
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function configureAutoUpdater(): void { function configureAutoUpdater(releaseChannel: AppReleaseChannel): void {
// Download updates in the background and only prompt once they are ready to install. // Download updates in the background and only prompt once they are ready to install.
autoUpdater.autoDownload = true; autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true; autoUpdater.autoInstallOnAppQuit = true;
// Suppress built-in dialogs; the renderer handles UI. // Suppress built-in dialogs; the renderer handles UI.
autoUpdater.autoRunAppAfterInstall = true; autoUpdater.autoRunAppAfterInstall = true;
autoUpdater.allowPrerelease = releaseChannel === "beta";
autoUpdater.channel = releaseChannel === "beta" ? "beta" : "latest";
autoUpdater.allowDowngrade = false;
if (configuredReleaseChannel !== releaseChannel) {
cachedUpdateInfo = null;
downloadedUpdateVersion = null;
downloading = false;
configuredReleaseChannel = releaseChannel;
}
if (autoUpdaterConfigured) { if (autoUpdaterConfigured) {
return; return;
@@ -109,7 +122,13 @@ function scheduleQuitAndInstall(onBeforeQuit?: () => Promise<void>): void {
// Public API // Public API
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export async function checkForAppUpdate(currentVersion: string): Promise<AppUpdateCheckResult> { export async function checkForAppUpdate({
currentVersion,
releaseChannel,
}: {
currentVersion: string;
releaseChannel: AppReleaseChannel;
}): Promise<AppUpdateCheckResult> {
if (!app.isPackaged) { if (!app.isPackaged) {
return buildCheckResult({ return buildCheckResult({
currentVersion, currentVersion,
@@ -118,7 +137,7 @@ export async function checkForAppUpdate(currentVersion: string): Promise<AppUpda
}); });
} }
configureAutoUpdater(); configureAutoUpdater(releaseChannel);
const cachedVersion = cachedUpdateInfo?.version ?? null; const cachedVersion = cachedUpdateInfo?.version ?? null;
if (cachedVersion && cachedVersion !== currentVersion) { if (cachedVersion && cachedVersion !== currentVersion) {
@@ -176,7 +195,13 @@ export async function checkForAppUpdate(currentVersion: string): Promise<AppUpda
} }
export async function downloadAndInstallUpdate( export async function downloadAndInstallUpdate(
currentVersion: string, {
currentVersion,
releaseChannel,
}: {
currentVersion: string;
releaseChannel: AppReleaseChannel;
},
onBeforeQuit?: () => Promise<void>, onBeforeQuit?: () => Promise<void>,
): Promise<AppUpdateInstallResult> { ): Promise<AppUpdateInstallResult> {
if (!app.isPackaged) { if (!app.isPackaged) {
@@ -195,7 +220,7 @@ export async function downloadAndInstallUpdate(
}; };
} }
configureAutoUpdater(); configureAutoUpdater(releaseChannel);
const readyVersion = cachedUpdateInfo.version; const readyVersion = cachedUpdateInfo.version;
if (isReadyToInstallVersion(readyVersion)) { if (isReadyToInstallVersion(readyVersion)) {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/expo-two-way-audio", "name": "@getpaseo/expo-two-way-audio",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"description": "Native module for two way audio streaming", "description": "Native module for two way audio streaming",
"main": "build/index.js", "main": "build/index.js",
"types": "build/index.d.ts", "types": "build/index.d.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/highlight", "name": "@getpaseo/highlight",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"type": "module", "type": "module",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/relay", "name": "@getpaseo/relay",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"description": "Paseo relay for bridging daemon and client connections", "description": "Paseo relay for bridging daemon and client connections",
"type": "module", "type": "module",
"publishConfig": { "publishConfig": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/server", "name": "@getpaseo/server",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"description": "Paseo backend server", "description": "Paseo backend server",
"type": "module", "type": "module",
"publishConfig": { "publishConfig": {
@@ -60,8 +60,8 @@
"@ai-sdk/openai": "2.0.52", "@ai-sdk/openai": "2.0.52",
"@anthropic-ai/claude-agent-sdk": "^0.2.11", "@anthropic-ai/claude-agent-sdk": "^0.2.11",
"@deepgram/sdk": "^3.4.0", "@deepgram/sdk": "^3.4.0",
"@getpaseo/highlight": "0.1.60-rc.3", "@getpaseo/highlight": "0.1.60-beta.1",
"@getpaseo/relay": "0.1.60-rc.3", "@getpaseo/relay": "0.1.60-beta.1",
"@isaacs/ttlcache": "^2.1.4", "@isaacs/ttlcache": "^2.1.4",
"@modelcontextprotocol/sdk": "^1.20.1", "@modelcontextprotocol/sdk": "^1.20.1",
"@mariozechner/pi-coding-agent": "^0.67.68", "@mariozechner/pi-coding-agent": "^0.67.68",

View File

@@ -234,7 +234,7 @@ const MIN_VERSION_FLEXIBLE_EDITOR_IDS = "0.1.50";
function isAppVersionAtLeast(appVersion: string | null, minVersion: string): boolean { function isAppVersionAtLeast(appVersion: string | null, minVersion: string): boolean {
if (!appVersion) return false; if (!appVersion) return false;
// Strip RC/prerelease suffix: "0.1.45-rc.4" "0.1.45" // Strip prerelease suffix: "0.1.45-beta.4" -> "0.1.45"
const base = appVersion.replace(/-.*$/, ""); const base = appVersion.replace(/-.*$/, "");
const parts = base.split(".").map(Number); const parts = base.split(".").map(Number);
const minParts = minVersion.split(".").map(Number); const minParts = minVersion.split(".").map(Number);

View File

@@ -1089,7 +1089,7 @@ describe("slugify", () => {
}); });
it("keeps very long names within the hostname label length limit", () => { it("keeps very long names within the hostname label length limit", () => {
const result = slugify("Release Candidate ".repeat(12)); const result = slugify("Beta Build ".repeat(12));
expect(result.length).toBeLessThanOrEqual(63); expect(result.length).toBeLessThanOrEqual(63);
expectValidHostnameLabel(result); expectValidHostnameLabel(result);
@@ -1107,7 +1107,7 @@ describe("slugify", () => {
"feature/cool stuff", "feature/cool stuff",
" Café Launch ", " Café Launch ",
"__bar__", "__bar__",
"Release Candidate ".repeat(12), "Beta Build ".repeat(12),
"release***candidate", "release***candidate",
]; ];

View File

@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/website", "name": "@getpaseo/website",
"version": "0.1.60-rc.3", "version": "0.1.60-beta.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -38,8 +38,9 @@ const entries = [
["RELEASE_BASE_VERSION", info.baseVersion], ["RELEASE_BASE_VERSION", info.baseVersion],
["RELEASE_PRERELEASE", info.prerelease ?? ""], ["RELEASE_PRERELEASE", info.prerelease ?? ""],
["IS_PRERELEASE", info.isPrerelease ? "true" : "false"], ["IS_PRERELEASE", info.isPrerelease ? "true" : "false"],
["IS_RELEASE_CANDIDATE", info.isReleaseCandidate ? "true" : "false"], ["IS_BETA", info.isBeta ? "true" : "false"],
["RELEASE_TYPE", info.releaseType], ["RELEASE_TYPE", info.releaseType],
["RELEASE_CHANNEL", info.releaseChannel],
["DESKTOP_VERSION", info.version], ["DESKTOP_VERSION", info.version],
["IS_SMOKE_TAG", info.isSmokeTag ? "true" : "false"], ["IS_SMOKE_TAG", info.isSmokeTag ? "true" : "false"],
]; ];

View File

@@ -14,7 +14,7 @@ export function parseReleaseVersion(version) {
const match = trimmed.match(versionPattern); const match = trimmed.match(versionPattern);
if (!match?.groups) { if (!match?.groups) {
throw new Error( throw new Error(
`Unsupported release version "${version}". Expected semver like 0.1.41 or 0.1.41-rc.1.`, `Unsupported release version "${version}". Expected semver like 0.1.41 or 0.1.41-beta.1.`,
); );
} }
@@ -22,14 +22,20 @@ export function parseReleaseVersion(version) {
const minor = Number.parseInt(match.groups.minor, 10); const minor = Number.parseInt(match.groups.minor, 10);
const patch = Number.parseInt(match.groups.patch, 10); const patch = Number.parseInt(match.groups.patch, 10);
const prerelease = match.groups.prerelease ?? null; const prerelease = match.groups.prerelease ?? null;
const rcMatch = prerelease?.match(/^rc\.(?<rc>\d+)$/) ?? null; const betaMatch = prerelease?.match(/^beta\.(?<beta>\d+)$/) ?? null;
const rcNumber = rcMatch?.groups?.rc ? Number.parseInt(rcMatch.groups.rc, 10) : null; const betaNumber = betaMatch?.groups?.beta ? Number.parseInt(betaMatch.groups.beta, 10) : null;
if (prerelease !== null && betaNumber === null) {
throw new Error(
`Unsupported release version "${version}". Expected beta prerelease versions like 0.1.41-beta.1.`,
);
}
assertInteger(major, "major version"); assertInteger(major, "major version");
assertInteger(minor, "minor version"); assertInteger(minor, "minor version");
assertInteger(patch, "patch version"); assertInteger(patch, "patch version");
if (rcNumber !== null) { if (betaNumber !== null) {
assertInteger(rcNumber, "release candidate number"); assertInteger(betaNumber, "beta number");
} }
return { return {
@@ -40,8 +46,8 @@ export function parseReleaseVersion(version) {
prerelease, prerelease,
baseVersion: `${major}.${minor}.${patch}`, baseVersion: `${major}.${minor}.${patch}`,
isPrerelease: prerelease !== null, isPrerelease: prerelease !== null,
isReleaseCandidate: rcNumber !== null, isBeta: betaNumber !== null,
rcNumber, betaNumber,
}; };
} }
@@ -57,7 +63,7 @@ export function normalizeReleaseTag(rawTag) {
const match = trimmed.match(sourceTagPattern); const match = trimmed.match(sourceTagPattern);
if (!match?.groups?.version) { if (!match?.groups?.version) {
throw new Error( throw new Error(
`Unsupported release tag "${rawTag}". Expected vX.Y.Z, vX.Y.Z-rc.N, desktop-v..., or android-v...`, `Unsupported release tag "${rawTag}". Expected vX.Y.Z, vX.Y.Z-beta.N, desktop-v..., or android-v...`,
); );
} }
return `v${match.groups.version}`; return `v${match.groups.version}`;
@@ -73,9 +79,10 @@ export function getReleaseInfoFromSourceTag(sourceTag) {
baseVersion: parsed.baseVersion, baseVersion: parsed.baseVersion,
prerelease: parsed.prerelease, prerelease: parsed.prerelease,
isPrerelease: parsed.isPrerelease, isPrerelease: parsed.isPrerelease,
isReleaseCandidate: parsed.isReleaseCandidate, isBeta: parsed.isBeta,
rcNumber: parsed.rcNumber, betaNumber: parsed.betaNumber,
releaseType: parsed.isPrerelease ? "prerelease" : "release", releaseType: parsed.isPrerelease ? "prerelease" : "release",
releaseChannel: parsed.isBeta ? "beta" : "latest",
isSmokeTag: sourceTag.includes("gha-smoke"), isSmokeTag: sourceTag.includes("gha-smoke"),
}; };
} }
@@ -110,54 +117,54 @@ export function computeNextReleaseVersion(currentVersion, mode) {
}); });
} }
if (mode === "rc-patch" || mode === "rc-minor" || mode === "rc-major") { if (mode === "beta-patch" || mode === "beta-minor" || mode === "beta-major") {
if (parsed.isPrerelease) { if (parsed.isPrerelease) {
throw new Error( throw new Error(
`Cannot start a new RC line from prerelease version ${currentVersion}. Use rc-next or promote.`, `Cannot start a new beta line from prerelease version ${currentVersion}. Use beta-next or promote.`,
); );
} }
if (mode === "rc-patch") { if (mode === "beta-patch") {
return formatReleaseVersion({ return formatReleaseVersion({
major: parsed.major, major: parsed.major,
minor: parsed.minor, minor: parsed.minor,
patch: parsed.patch + 1, patch: parsed.patch + 1,
prerelease: "rc.1", prerelease: "beta.1",
}); });
} }
if (mode === "rc-minor") { if (mode === "beta-minor") {
return formatReleaseVersion({ return formatReleaseVersion({
major: parsed.major, major: parsed.major,
minor: parsed.minor + 1, minor: parsed.minor + 1,
patch: 0, patch: 0,
prerelease: "rc.1", prerelease: "beta.1",
}); });
} }
return formatReleaseVersion({ return formatReleaseVersion({
major: parsed.major + 1, major: parsed.major + 1,
minor: 0, minor: 0,
patch: 0, patch: 0,
prerelease: "rc.1", prerelease: "beta.1",
}); });
} }
if (mode === "rc-next") { if (mode === "beta-next") {
if (!parsed.isReleaseCandidate || parsed.rcNumber === null) { if (!parsed.isBeta || parsed.betaNumber === null) {
throw new Error( throw new Error(
`Cannot advance RC number from ${currentVersion}. Expected a version like 0.1.41-rc.1.`, `Cannot advance beta number from ${currentVersion}. Expected a version like 0.1.41-beta.1.`,
); );
} }
return formatReleaseVersion({ return formatReleaseVersion({
major: parsed.major, major: parsed.major,
minor: parsed.minor, minor: parsed.minor,
patch: parsed.patch, patch: parsed.patch,
prerelease: `rc.${parsed.rcNumber + 1}`, prerelease: `beta.${parsed.betaNumber + 1}`,
}); });
} }
if (mode === "promote") { if (mode === "promote") {
if (!parsed.isReleaseCandidate) { if (!parsed.isBeta) {
throw new Error( throw new Error(
`Cannot promote ${currentVersion}. Expected a release candidate version like 0.1.41-rc.1.`, `Cannot promote ${currentVersion}. Expected a beta version like 0.1.41-beta.1.`,
); );
} }
return parsed.baseVersion; return parsed.baseVersion;

View File

@@ -0,0 +1,53 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
computeNextReleaseVersion,
getReleaseInfoFromSourceTag,
parseReleaseVersion,
} from "./release-version-utils.mjs";
test("computes the next beta patch from a stable version", () => {
assert.equal(computeNextReleaseVersion("0.1.59", "beta-patch"), "0.1.60-beta.1");
});
test("advances beta versions", () => {
assert.equal(computeNextReleaseVersion("0.1.60-beta.1", "beta-next"), "0.1.60-beta.2");
});
test("promotes beta versions to stable", () => {
assert.equal(computeNextReleaseVersion("0.1.60-beta.2", "promote"), "0.1.60");
});
test("parses beta release metadata", () => {
assert.deepEqual(parseReleaseVersion("0.1.60-beta.1"), {
version: "0.1.60-beta.1",
major: 0,
minor: 1,
patch: 60,
prerelease: "beta.1",
baseVersion: "0.1.60",
isPrerelease: true,
isBeta: true,
betaNumber: 1,
});
});
test("emits beta release info from tags", () => {
assert.deepEqual(getReleaseInfoFromSourceTag("v0.1.60-beta.1"), {
sourceTag: "v0.1.60-beta.1",
releaseTag: "v0.1.60-beta.1",
version: "0.1.60-beta.1",
baseVersion: "0.1.60",
prerelease: "beta.1",
isPrerelease: true,
isBeta: true,
betaNumber: 1,
releaseType: "prerelease",
releaseChannel: "beta",
isSmokeTag: false,
});
});
test("rejects non-beta prerelease versions", () => {
assert.throws(() => parseReleaseVersion("0.1.60-canary.1"), /Expected beta prerelease versions/);
});

View File

@@ -11,7 +11,7 @@ const rootPackagePath = path.join(rootDir, "package.json");
function usageAndExit(code = 1) { function usageAndExit(code = 1) {
process.stderr.write(`Usage: node scripts/set-release-version.mjs --mode <mode> [--print]\n`); process.stderr.write(`Usage: node scripts/set-release-version.mjs --mode <mode> [--print]\n`);
process.stderr.write( process.stderr.write(
"Modes: patch, minor, major, rc-patch, rc-minor, rc-major, rc-next, promote\n", "Modes: patch, minor, major, beta-patch, beta-minor, beta-major, beta-next, promote\n",
); );
process.exit(code); process.exit(code);
} }

View File

@@ -139,24 +139,6 @@ function updateReleaseNotes({ releaseId, repo, notesPath }, execFileSync = nodeE
); );
} }
function buildPrereleaseNotes({ releaseTag, version }) {
const today = new Date().toISOString().slice(0, 10);
const lines = [
`## ${version} - ${today}`,
"",
"Release candidate build for testing from `main`.",
"",
`- Tag: \`${releaseTag}\``,
];
if (process.env.GITHUB_SHA) {
lines.push(`- Commit: \`${process.env.GITHUB_SHA}\``);
}
lines.push("- This prerelease does not replace the website's latest stable download.");
return `${lines.join("\n")}\n`;
}
export function syncReleaseNotes(argv = process.argv.slice(2), deps = {}) { export function syncReleaseNotes(argv = process.argv.slice(2), deps = {}) {
const execFileSync = deps.execFileSync ?? nodeExecFileSync; const execFileSync = deps.execFileSync ?? nodeExecFileSync;
const args = parseArgs(argv); const args = parseArgs(argv);
@@ -170,13 +152,6 @@ export function syncReleaseNotes(argv = process.argv.slice(2), deps = {}) {
let notes = targetEntry?.notes ?? null; let notes = targetEntry?.notes ?? null;
if (!notes && releaseInfo.isPrerelease) {
notes = buildPrereleaseNotes({
releaseTag: releaseInfo.releaseTag,
version: releaseInfo.version,
});
}
if (!notes) { if (!notes) {
console.log(`No matching changelog section found for ${targetTag}. Skipping.`); console.log(`No matching changelog section found for ${targetTag}. Skipping.`);
return; return;

View File

@@ -9,7 +9,7 @@ function withTempChangelog(fn) {
const previousCwd = process.cwd(); const previousCwd = process.cwd();
const tempDir = mkdtempSync(path.join(tmpdir(), "paseo-release-notes-test-")); const tempDir = mkdtempSync(path.join(tmpdir(), "paseo-release-notes-test-"));
process.chdir(tempDir); process.chdir(tempDir);
writeFileSync("CHANGELOG.md", "## 0.1.60 - 2026-04-20\n\n- Stable notes.\n"); writeFileSync("CHANGELOG.md", "## 0.1.60-beta.1 - 2026-04-20\n\n- Beta notes.\n");
try { try {
fn(); fn();
@@ -26,7 +26,7 @@ test("updates an existing release body through the release id API", () => {
const execFileSync = (command, args, options) => { const execFileSync = (command, args, options) => {
calls.push({ args, command, options }); calls.push({ args, command, options });
if (args[0] === "api" && args[1] === "repos/getpaseo/paseo/releases/tags/v0.1.60-rc.3") { if (args[0] === "api" && args[1] === "repos/getpaseo/paseo/releases/tags/v0.1.60-beta.1") {
return JSON.stringify({ id: 311163621 }); return JSON.stringify({ id: 311163621 });
} }
@@ -34,14 +34,14 @@ test("updates an existing release body through the release id API", () => {
const notesArg = args.find((arg) => arg.startsWith("body=@")); const notesArg = args.find((arg) => arg.startsWith("body=@"));
assert.ok(notesArg, "PATCH should send the notes body from a file"); assert.ok(notesArg, "PATCH should send the notes body from a file");
const notesPath = notesArg.slice("body=@".length); const notesPath = notesArg.slice("body=@".length);
assert.match(notesPath, /v0\.1\.60-rc\.3-notes\.md$/); assert.match(notesPath, /v0\.1\.60-beta\.1-notes\.md$/);
return ""; return "";
} }
throw new Error(`Unexpected gh call: ${command} ${args.join(" ")}`); throw new Error(`Unexpected gh call: ${command} ${args.join(" ")}`);
}; };
syncReleaseNotes(["--repo", "getpaseo/paseo", "--tag", "v0.1.60-rc.3"], { syncReleaseNotes(["--repo", "getpaseo/paseo", "--tag", "v0.1.60-beta.1"], {
execFileSync, execFileSync,
}); });