mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Simplify desktop release to tag-and-push (#185)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
31
.github/workflows/sprout-desktop-release.yml
vendored
31
.github/workflows/sprout-desktop-release.yml
vendored
@@ -15,6 +15,8 @@ jobs:
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
env:
|
||||
RELEASE_VERSION: '' # set in the "Extract version from tag" step
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
@@ -27,16 +29,19 @@ jobs:
|
||||
workspaces: desktop/src-tauri
|
||||
key: desktop-release-aarch64-apple-darwin
|
||||
|
||||
- name: Validate tag matches desktop versions
|
||||
- name: Extract version from tag
|
||||
run: |
|
||||
TAG_VERSION="${GITHUB_REF#refs/tags/desktop/v}"
|
||||
PACKAGE_VERSION="$(node -p "require('./desktop/package.json').version")"
|
||||
TAURI_VERSION="$(node -p "require('./desktop/src-tauri/tauri.conf.json').version")"
|
||||
CARGO_VERSION="$(grep '^version' desktop/src-tauri/Cargo.toml | head -1 | sed 's/version = "//;s/"//')"
|
||||
if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ] || [ "$TAG_VERSION" != "$TAURI_VERSION" ] || [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then
|
||||
echo "::error::Tag version ($TAG_VERSION) must match package.json ($PACKAGE_VERSION), tauri.conf.json ($TAURI_VERSION), and Cargo.toml ($CARGO_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
RELEASE_VERSION="${GITHUB_REF#refs/tags/desktop/v}"
|
||||
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_ENV"
|
||||
echo "Release version: ${RELEASE_VERSION}"
|
||||
|
||||
- name: Set version from tag
|
||||
working-directory: desktop
|
||||
run: node scripts/set-version-from-tag.mjs "$RELEASE_VERSION"
|
||||
|
||||
- name: Regenerate Cargo lockfile
|
||||
working-directory: desktop/src-tauri
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Validate release secrets
|
||||
env:
|
||||
@@ -97,14 +102,13 @@ jobs:
|
||||
|
||||
- name: Create DMG from signed app
|
||||
run: |
|
||||
VERSION="$(node -p "require('./desktop/package.json').version")"
|
||||
DMG_DIR="desktop/src-tauri/target/release/bundle/dmg"
|
||||
mkdir -p "${DMG_DIR}"
|
||||
rm -f "${DMG_DIR}"/*.dmg
|
||||
hdiutil create -volname "Sprout" \
|
||||
-srcfolder desktop/src-tauri/target/release/bundle/macos/Sprout.app \
|
||||
-ov -format UDZO \
|
||||
"${DMG_DIR}/Sprout_${VERSION}_aarch64.dmg"
|
||||
"${DMG_DIR}/Sprout_${RELEASE_VERSION}_aarch64.dmg"
|
||||
|
||||
- name: Create updater archive from signed app
|
||||
env:
|
||||
@@ -120,10 +124,9 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
VERSION="${GITHUB_REF#refs/tags/desktop/v}"
|
||||
gh release create "$GITHUB_REF_NAME" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "Sprout Desktop v${VERSION}" \
|
||||
--title "Sprout Desktop v${RELEASE_VERSION}" \
|
||||
--notes "See the assets to download and install this version."
|
||||
|
||||
- name: Publish updater alias
|
||||
@@ -131,6 +134,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
VERSION: ${{ env.RELEASE_VERSION }}
|
||||
run: pnpm run release:updater:publish
|
||||
|
||||
- name: Publish DMG alias
|
||||
@@ -138,4 +142,5 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
VERSION: ${{ env.RELEASE_VERSION }}
|
||||
run: pnpm run release:dmg:publish
|
||||
|
||||
@@ -38,42 +38,29 @@ Store the password you chose in `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`.
|
||||
|
||||
## Cutting a Release
|
||||
|
||||
### 1. Prepare
|
||||
|
||||
From `main`, run:
|
||||
From any branch (typically `main`):
|
||||
|
||||
```bash
|
||||
just desktop-prepare <version>
|
||||
just desktop-release 0.3.0
|
||||
```
|
||||
|
||||
For example:
|
||||
Or equivalently:
|
||||
|
||||
```bash
|
||||
just desktop-prepare 0.2.0
|
||||
git tag desktop/v0.3.0
|
||||
git push origin desktop/v0.3.0
|
||||
```
|
||||
|
||||
This creates a release branch, bumps versions in `package.json`,
|
||||
`tauri.conf.json`, and `Cargo.toml`, and opens a PR.
|
||||
That's it. CI extracts the version from the tag and writes it into
|
||||
`package.json`, `tauri.conf.json`, and `Cargo.toml` at build time. The
|
||||
versions checked into the repo are not used for releases — the tag is the
|
||||
source of truth.
|
||||
|
||||
### 2. Review & Merge
|
||||
|
||||
Review the PR, ensure CI passes, then merge to `main`.
|
||||
|
||||
### 3. Release
|
||||
|
||||
From `main` (after pulling the merged changes), run:
|
||||
|
||||
```bash
|
||||
just desktop-release <version>
|
||||
```
|
||||
|
||||
This tags the commit and pushes the tag — CI handles the rest.
|
||||
|
||||
### 4. Verify
|
||||
### Verify
|
||||
|
||||
Check GitHub Releases for:
|
||||
|
||||
- The **versioned release** (e.g. `sprout-desktop-v0.2.0`)
|
||||
- The **versioned release** (e.g. `desktop/v0.3.0`)
|
||||
- The **`sprout-desktop-latest` rolling release** (updated with every release)
|
||||
|
||||
---
|
||||
@@ -82,17 +69,19 @@ Check GitHub Releases for:
|
||||
|
||||
The `sprout-desktop-release.yml` workflow:
|
||||
|
||||
1. **Validates** the tag version matches the version in `package.json`,
|
||||
`tauri.conf.json`, and `Cargo.toml`.
|
||||
2. **Validates** all required secrets are present.
|
||||
3. **Builds** the release config with signing and updater settings.
|
||||
4. **Builds** the Tauri app (unsigned).
|
||||
5. **Signs and notarizes** the macOS bundle via `block/apple-codesign-action`.
|
||||
6. **Re-packages** the signed app into a DMG and updater archive.
|
||||
7. **Signs** the updater archive with the Tauri updater key.
|
||||
8. **Publishes** the updater manifest (`latest.json`) to the rolling
|
||||
`sprout-desktop-latest` release.
|
||||
9. **Publishes** the DMG to both the versioned and rolling releases.
|
||||
1. **Extracts the version** from the git tag once into `RELEASE_VERSION`.
|
||||
2. **Sets the version** into `package.json`, `tauri.conf.json`, and
|
||||
`Cargo.toml` using `set-version-from-tag.mjs`.
|
||||
3. **Regenerates `Cargo.lock`** to match the patched `Cargo.toml`.
|
||||
4. **Validates** all required secrets are present.
|
||||
5. **Builds** the release config with signing and updater settings.
|
||||
6. **Builds** the Tauri app (unsigned).
|
||||
7. **Signs and notarizes** the macOS bundle via `block/apple-codesign-action`.
|
||||
8. **Re-packages** the signed app into a DMG and updater archive.
|
||||
9. **Signs** the updater archive with the Tauri updater key.
|
||||
10. **Publishes** the updater manifest (`latest.json`) to the rolling
|
||||
`sprout-desktop-latest` release.
|
||||
11. **Publishes** the DMG to both the versioned and rolling releases.
|
||||
|
||||
---
|
||||
|
||||
@@ -111,8 +100,14 @@ export SPROUT_UPDATER_ENDPOINT="https://github.com/block/sprout/releases/downloa
|
||||
cd desktop
|
||||
pnpm run tauri:release:config
|
||||
|
||||
# Build (unsigned)
|
||||
just desktop-release-build
|
||||
# Build (unsigned) — pass a version to set it before building
|
||||
just desktop-release-build version=0.3.0
|
||||
```
|
||||
|
||||
You can also set the version separately without building:
|
||||
|
||||
```bash
|
||||
just desktop-set-version 0.3.0
|
||||
```
|
||||
|
||||
---
|
||||
@@ -142,6 +137,20 @@ The app connects to the relay via the `SPROUT_RELAY_URL` environment variable.
|
||||
|
||||
---
|
||||
|
||||
## How Versioning Works
|
||||
|
||||
The git tag is the single source of truth for the release version. The version
|
||||
fields in `package.json`, `tauri.conf.json`, and `Cargo.toml` on `main` are
|
||||
**not** used for releases — CI overwrites them at build time from the tag.
|
||||
|
||||
This means the tagged commit will show a different version in its source files
|
||||
than what the release actually contains. This is an accepted tradeoff — the tag
|
||||
is the canonical version, the commit is just the code state at release time.
|
||||
This is standard practice in ecosystems like Docker, Go, and Rust where the
|
||||
tag drives the version.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Missing required desktop release secrets"**: Ensure all secrets listed in
|
||||
@@ -152,6 +161,6 @@ The app connects to the relay via the `SPROUT_RELAY_URL` environment variable.
|
||||
`CODESIGN_S3_BUCKET` are configured correctly. Check the
|
||||
`block/apple-codesign-action` step logs for details.
|
||||
|
||||
- **Version mismatch**: The tag version must exactly match all three version
|
||||
files (`package.json`, `tauri.conf.json`, `Cargo.toml`). Use
|
||||
`just desktop-prepare` to ensure consistency.
|
||||
- **Build failures**: If versions are wrong, check that the tag follows the
|
||||
format `desktop/v<semver>` (e.g. `desktop/v0.3.0`). CI extracts the version
|
||||
from the tag automatically.
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const version = process.argv[2];
|
||||
|
||||
if (!version) {
|
||||
console.error("Usage: node scripts/bump-version.mjs <version>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
||||
console.error(
|
||||
`Invalid version "${version}". Expected semver format (e.g. 1.2.3 or 1.2.3-beta.1)`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const packageJsonPath = resolve(process.cwd(), "package.json");
|
||||
const tauriConfigPath = resolve(process.cwd(), "src-tauri/tauri.conf.json");
|
||||
const cargoTomlPath = resolve(process.cwd(), "src-tauri/Cargo.toml");
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
||||
if (packageJson.version !== version) {
|
||||
packageJson.version = version;
|
||||
writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
console.log(`Updated package.json to ${version}`);
|
||||
} else {
|
||||
console.log(`package.json already at ${version}`);
|
||||
}
|
||||
|
||||
const tauriConfig = JSON.parse(readFileSync(tauriConfigPath, "utf8"));
|
||||
if (tauriConfig.version !== version) {
|
||||
tauriConfig.version = version;
|
||||
writeFileSync(tauriConfigPath, `${JSON.stringify(tauriConfig, null, 2)}\n`);
|
||||
console.log(`Updated tauri.conf.json to ${version}`);
|
||||
} else {
|
||||
console.log(`tauri.conf.json already at ${version}`);
|
||||
}
|
||||
|
||||
const cargoToml = readFileSync(cargoTomlPath, "utf8");
|
||||
const currentCargoVersion = cargoToml.match(/^version = "(.*)"$/m)?.[1];
|
||||
if (!currentCargoVersion) {
|
||||
throw new Error(`Could not find version field in ${cargoTomlPath}`);
|
||||
}
|
||||
if (currentCargoVersion !== version) {
|
||||
const updatedCargoToml = cargoToml.replace(
|
||||
/^version = ".*"$/m,
|
||||
`version = "${version}"`,
|
||||
);
|
||||
writeFileSync(cargoTomlPath, updatedCargoToml);
|
||||
console.log(`Updated Cargo.toml to ${version}`);
|
||||
} else {
|
||||
console.log(`Cargo.toml already at ${version}`);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { join, resolve } from "node:path";
|
||||
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { cpSync, existsSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const repo = process.env.GITHUB_REPOSITORY ?? "block/sprout";
|
||||
const version = process.env.VERSION ?? readVersionFromConfig();
|
||||
const version = requireVersionEnv();
|
||||
const versionTag = `desktop/v${version}`;
|
||||
const latestTag = "sprout-desktop-latest";
|
||||
const dryRun = process.env.DRY_RUN === "true" || process.env.DRY_RUN === "1";
|
||||
@@ -26,14 +26,14 @@ const dmgDir = resolve(
|
||||
);
|
||||
const dmgPath = join(dmgDir, dmgName);
|
||||
|
||||
function readVersionFromConfig() {
|
||||
const configPath = resolve(process.cwd(), "src-tauri/tauri.conf.json");
|
||||
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
||||
const configVersion = config?.version;
|
||||
if (typeof configVersion !== "string" || !configVersion.trim()) {
|
||||
throw new Error(`Could not determine version from ${configPath}`);
|
||||
function requireVersionEnv() {
|
||||
const v = process.env.VERSION;
|
||||
if (!v || !v.trim()) {
|
||||
throw new Error(
|
||||
"VERSION env var is required. CI sets this from the git tag; for local use, run: VERSION=x.y.z pnpm run release:dmg:publish",
|
||||
);
|
||||
}
|
||||
return configVersion;
|
||||
return v.trim();
|
||||
}
|
||||
|
||||
function quote(arg) {
|
||||
|
||||
@@ -12,8 +12,7 @@ import { tmpdir } from "node:os";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
|
||||
const repo = process.env.GITHUB_REPOSITORY ?? "block/sprout";
|
||||
const tauriConfigPath = resolve(process.cwd(), "src-tauri/tauri.conf.json");
|
||||
const version = process.env.VERSION ?? readVersionFromConfig();
|
||||
const version = requireVersionEnv();
|
||||
const latestTag = "sprout-desktop-latest";
|
||||
const tauriTarget = process.env.TAURI_TARGET ?? "aarch64-apple-darwin";
|
||||
const updaterPlatform = process.env.UPDATER_PLATFORM ?? "darwin-aarch64";
|
||||
@@ -31,13 +30,14 @@ const bundleDir = resolve(
|
||||
);
|
||||
const latestPath = join(bundleDir, "latest.json");
|
||||
|
||||
function readVersionFromConfig() {
|
||||
const config = JSON.parse(readFileSync(tauriConfigPath, "utf-8"));
|
||||
const configVersion = config?.version;
|
||||
if (typeof configVersion !== "string" || !configVersion.trim()) {
|
||||
throw new Error(`Could not determine version from ${tauriConfigPath}`);
|
||||
function requireVersionEnv() {
|
||||
const v = process.env.VERSION;
|
||||
if (!v || !v.trim()) {
|
||||
throw new Error(
|
||||
"VERSION env var is required. CI sets this from the git tag; for local use, run: VERSION=x.y.z pnpm run release:updater:publish",
|
||||
);
|
||||
}
|
||||
return configVersion;
|
||||
return v.trim();
|
||||
}
|
||||
|
||||
function requirePath(path) {
|
||||
|
||||
38
desktop/scripts/set-version-from-tag.mjs
Normal file
38
desktop/scripts/set-version-from-tag.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const version = process.argv[2];
|
||||
|
||||
if (!version) {
|
||||
console.error("Usage: node scripts/set-version-from-tag.mjs <version>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
||||
console.error(
|
||||
`Invalid version "${version}". Expected semver format (e.g. 1.2.3 or 1.2.3-beta.1)`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const packageJsonPath = resolve(process.cwd(), "package.json");
|
||||
const tauriConfigPath = resolve(process.cwd(), "src-tauri/tauri.conf.json");
|
||||
const cargoTomlPath = resolve(process.cwd(), "src-tauri/Cargo.toml");
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
||||
packageJson.version = version;
|
||||
writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
console.log(`Set package.json to ${version}`);
|
||||
|
||||
const tauriConfig = JSON.parse(readFileSync(tauriConfigPath, "utf8"));
|
||||
tauriConfig.version = version;
|
||||
writeFileSync(tauriConfigPath, `${JSON.stringify(tauriConfig, null, 2)}\n`);
|
||||
console.log(`Set tauri.conf.json to ${version}`);
|
||||
|
||||
const cargoToml = readFileSync(cargoTomlPath, "utf8");
|
||||
const updatedCargoToml = cargoToml.replace(
|
||||
/^version = ".*"$/m,
|
||||
`version = "${version}"`,
|
||||
);
|
||||
writeFileSync(cargoTomlPath, updatedCargoToml);
|
||||
console.log(`Set Cargo.toml to ${version}`);
|
||||
74
justfile
74
justfile
@@ -153,72 +153,28 @@ desktop-app *ARGS:
|
||||
|
||||
# ─── Desktop Release ──────────────────────────────────────────────────────────
|
||||
|
||||
# Create a release branch, bump desktop versions, and open a release PR
|
||||
desktop-prepare version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
current_branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$current_branch" != "main" ]; then
|
||||
echo "Error: desktop-prepare must be run from the main branch (currently on '$current_branch')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$(git status --short)" ]; then
|
||||
echo "Error: working tree must be clean before preparing a release." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
branch="release/desktop-v{{version}}"
|
||||
|
||||
git pull --ff-only origin main
|
||||
git switch -c "$branch"
|
||||
|
||||
cd desktop
|
||||
node scripts/bump-version.mjs "{{version}}"
|
||||
cd src-tauri && cargo generate-lockfile && cd ..
|
||||
|
||||
git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml src-tauri/Cargo.lock
|
||||
git commit -m "release: desktop v{{version}}"
|
||||
git push --set-upstream origin "$branch"
|
||||
|
||||
gh pr create \
|
||||
--title "release: desktop v{{version}}" \
|
||||
--body "Release desktop v{{version}}. Merge this PR, then run \`just desktop-release {{version}}\` from main to tag and publish." \
|
||||
--base main \
|
||||
--head "$branch"
|
||||
|
||||
echo ""
|
||||
echo "Release PR created. Once merged, run 'just desktop-release {{version}}' on main."
|
||||
|
||||
# Tag and push the merged release commit to trigger the desktop release workflow
|
||||
# Tag and push to trigger the desktop release workflow (CI handles version bumping)
|
||||
desktop-release version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
current_branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$current_branch" != "main" ]; then
|
||||
echo "Error: desktop-release must be run from the main branch (currently on '$current_branch')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git pull --ff-only origin main
|
||||
|
||||
expected_msg="release: desktop v{{version}}"
|
||||
release_sha=$(git log --format="%H %s" main | grep -F "$expected_msg" | head -1 | cut -d' ' -f1 || true)
|
||||
if [ -z "$release_sha" ]; then
|
||||
echo "Error: could not find commit '$expected_msg' on main." >&2
|
||||
echo "Make sure the release PR has been merged and you've pulled latest main." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git tag "desktop/v{{version}}" "$release_sha"
|
||||
git tag "desktop/v{{version}}"
|
||||
git push origin "desktop/v{{version}}"
|
||||
|
||||
echo "Pushed tag desktop/v{{version}} — CI will build and publish the release."
|
||||
echo "Pushed tag desktop/v{{version}} — CI will set the version, build, and publish the release."
|
||||
|
||||
# Build a signed desktop release locally (for testing)
|
||||
desktop-release-build target="aarch64-apple-darwin" *args:
|
||||
# Set the desktop app version (patches package.json, tauri.conf.json, Cargo.toml)
|
||||
desktop-set-version version:
|
||||
cd {{desktop_dir}} && node scripts/set-version-from-tag.mjs "{{version}}"
|
||||
cd {{desktop_dir}}/src-tauri && cargo generate-lockfile
|
||||
|
||||
# Build a local desktop release (for testing). Optionally set a version first.
|
||||
desktop-release-build version="" target="aarch64-apple-darwin" *args:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [ -n "{{version}}" ]; then
|
||||
just desktop-set-version "{{version}}"
|
||||
fi
|
||||
cd {{desktop_dir}} && pnpm exec tauri build --target {{target}} --config src-tauri/tauri.release.conf.json {{args}}
|
||||
|
||||
# ─── Database ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user