diff --git a/.github/scripts/make-release-files.mjs b/.github/scripts/make-release-files.mjs new file mode 100644 index 0000000..a5529ea --- /dev/null +++ b/.github/scripts/make-release-files.mjs @@ -0,0 +1,53 @@ +// Writes the two files a release publishes besides the installer itself: +// +// latest.json what the app's update check reads +// (releases/latest/download/latest.json): the newest version, +// its notes, and where to download the installer, with the +// installer's SHA-256 so an in-app updater can verify what it +// downloaded. +// notes.md the release description on GitHub. +// +// Notes come from the app's own changelog (gpui/js/dist/changelog.json, +// produced by gpui/build.rs from gpui/js/lib/changelog.ts), so the release +// page and the in-app "What's new" always say the same thing. +// +// Usage: node make-release-files.mjs + +import { createHash } from "node:crypto"; +import { readFileSync, statSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +const [tag, installerPath, repo, outDir] = process.argv.slice(2); +if (!tag || !installerPath || !repo || !outDir) { + console.error("usage: make-release-files.mjs "); + process.exit(2); +} + +const version = tag.replace(/^v/, ""); +const changelog = JSON.parse(readFileSync("gpui/js/dist/changelog.json", "utf8")); +const entry = changelog.find((item) => item.version === version); +if (!entry) { + console.error(`gpui/js/lib/changelog.ts has no entry for ${version}; add one before releasing.`); + process.exit(1); +} + +const notes = entry.changes.map((change) => `- ${change}`).join("\n"); +const installer = readFileSync(installerPath); +const asset = path.basename(installerPath); + +const latest = { + version, + notes, + pub_date: new Date().toISOString(), + platforms: { + "windows-x86_64": { + url: `https://github.com/${repo}/releases/download/${tag}/${asset}`, + sha256: createHash("sha256").update(installer).digest("hex"), + size: statSync(installerPath).size, + }, + }, +}; + +writeFileSync(path.join(outDir, "latest.json"), `${JSON.stringify(latest, null, 2)}\n`); +writeFileSync(path.join(outDir, "notes.md"), `${notes}\n`); +console.log(`wrote latest.json and notes.md for ${version}`); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0443d4..329fdd9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,14 +1,27 @@ name: Release -# Publishes a GitHub Release with the Windows installer. Push a tag like -# v0.1.0 (or run this by hand and give it a tag) to cut one. The app's own -# update check reads releases/latest, so this must publish a real, -# non-draft, non-prerelease release. +# Publishes a GitHub Release for a version tag. Each release carries: +# +# OpenMouseDesktopSetup.exe the Windows installer. The file name never +# changes, so +# releases/latest/download/OpenMouseDesktopSetup.exe +# is a permanent "download the newest" link. +# latest.json what the app's update check reads +# (releases/latest/download/latest.json): version, +# notes, and the installer's URL and SHA-256. +# +# To release: bump `version` in gpui/Cargo.toml (and gpui/installer/Cargo.toml), +# add that version's entry to gpui/js/lib/changelog.ts, merge, then push a tag +# like v0.1.0 (or run this by hand with that tag). The tag, Cargo version and +# changelog entry must all agree or the run stops before publishing. +# +# `releases/latest` only resolves to the newest non-draft, non-prerelease +# release, so this publishes as one. on: workflow_dispatch: inputs: tag: - description: "Tag to release, e.g. v0.1.0" + description: "Tag to release, e.g. v0.1.0 (created from the selected branch if it does not exist)" required: true push: tags: @@ -20,9 +33,22 @@ permissions: jobs: release: runs-on: windows-latest + env: + TAG: ${{ github.event.inputs.tag || github.ref_name }} steps: - uses: actions/checkout@v4 + - name: Check the tag matches the app version + shell: bash + run: | + version="${TAG#v}" + cargo_version=$(sed -n 's/^version = "\(.*\)"/\1/p' gpui/Cargo.toml | head -1) + installer_version=$(sed -n 's/^version = "\(.*\)"/\1/p' gpui/installer/Cargo.toml | head -1) + if [ "$version" != "$cargo_version" ] || [ "$version" != "$installer_version" ]; then + echo "::error::Tag $TAG, gpui/Cargo.toml ($cargo_version) and gpui/installer/Cargo.toml ($installer_version) must all be the same version." + exit 1 + fi + - uses: actions/setup-node@v4 with: node-version: lts/* @@ -37,6 +63,10 @@ jobs: - name: Install JS dependencies run: npm ci + - name: Test + run: cargo test + working-directory: gpui + - name: Build release binary run: cargo build --release working-directory: gpui @@ -46,15 +76,22 @@ jobs: run: cargo build --release working-directory: gpui/installer - - name: Rename installer - run: cp gpui/installer/target/release/openmouse-installer.exe OpenMouseDesktopSetup.exe + - name: Collect release files + shell: bash + run: | + mkdir release + cp gpui/installer/target/release/openmouse-installer.exe release/OpenMouseDesktopSetup.exe + node .github/scripts/make-release-files.mjs "$TAG" release/OpenMouseDesktopSetup.exe "$GITHUB_REPOSITORY" release + cat release/latest.json - name: Publish release shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ github.event.inputs.tag || github.ref_name }} run: | - gh release create "$TAG" OpenMouseDesktopSetup.exe \ + gh release create "$TAG" \ + release/OpenMouseDesktopSetup.exe release/latest.json \ --title "OpenMouse Desktop $TAG" \ - --notes "See Settings → Changelog in the app for details." + --notes-file release/notes.md \ + --target "$GITHUB_SHA" \ + --latest diff --git a/gpui/src/update_check.rs b/gpui/src/update_check.rs index 7f8fe8f..30d947f 100644 --- a/gpui/src/update_check.rs +++ b/gpui/src/update_check.rs @@ -1,7 +1,7 @@ -//! Update availability check against the GitHub Releases `latest.json` -//! endpoint (the same one `src-tauri/tauri.conf.json`'s updater plugin uses), -//! mirroring `src/lib/update-check.ts` — but check-only for now: no signature -//! verification, download, or install here, just "is a newer version out". +//! Update availability check against the `latest.json` that each GitHub Release +//! publishes (see `.github/workflows/release.yml`). Check-only for now: it says +//! whether a newer version is out. The file also carries the installer URL and +//! its SHA-256, which is what a future in-app download and install will use. use std::time::Duration;