Skip to content

Release Desktop App #53

Release Desktop App

Release Desktop App #53

name: Release Desktop App
permissions:
contents: write
on:
push:
tags:
- 'v[0-9]*'
- '!v*-rc.*'
- '!v*-beta.*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g. v0.2.0)'
required: true
jobs:
# Create the GitHub Release first so build jobs don't race
create-release:
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create.outputs.result }}
steps:
- uses: actions/checkout@v4
- name: Extract release notes from tag
id: tag_notes
run: |
TAG="${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}"
VERSION="${TAG#v}"
REPO="${{ github.repository }}"
# User-authored tag annotation (if any). When absent, the Download
# section below stands on its own so the release page never falls
# back to a generic "see the assets" stub.
NOTES=$(git tag -l --format='%(contents)' "$TAG" | sed '/^$/d')
# The "## Download" block (per-platform install links + npm line) is
# generated by scripts/release/release-assets.mjs — the single source
# of truth shared with the asset-rename step below, so every link
# always resolves to a shipped asset name. Run with the runner's
# preinstalled node (no npm install / tsx needed for this .mjs).
{
if [ -n "$NOTES" ]; then
echo "$NOTES"
echo ""
fi
node scripts/release/release-assets.mjs download-section "$VERSION" "$REPO" "$TAG"
} > /tmp/release-notes.md
echo "body<<ENDOFNOTES" >> "$GITHUB_OUTPUT"
cat /tmp/release-notes.md >> "$GITHUB_OUTPUT"
# /tmp/release-notes.md ends with `release-assets.mjs download-section`,
# whose stdout has no trailing newline (it returns lines.join('\n')).
# Without this explicit newline the closing delimiter glues onto the
# last notes line, so GitHub never sees a standalone `ENDOFNOTES` and
# rejects the step output with "Matching delimiter not found".
echo "" >> "$GITHUB_OUTPUT"
echo "ENDOFNOTES" >> "$GITHUB_OUTPUT"
# GB-779 — always create as a draft. A non-draft release is immediately
# visible to the public AND immediately flips GitHub's `releases/latest`
# pointer, but the platform `build` matrix below takes 10–30 minutes to
# upload bundles + rename DMGs to their friendly names. The Tauri
# updater (`src-tauri/tauri.conf.json` endpoint
# `/releases/latest/download/latest.json`) resolves via that flipped
# pointer and starts serving the half-published release to real users
# mid-build. GitHub skips drafts for both the public Releases page AND
# the `latest` pointer, so keeping the release as a draft until the
# very end holds the entire publication atomic — the prior stable
# release remains the user-visible "latest" right up until the final
# `publish-release` job flips `draft:false`. Maintainers can still see
# the in-progress draft at /releases while logged in.
- name: Create release (as draft — flipped by publish-release at end)
id: create
uses: actions/github-script@v7
env:
RELEASE_BODY: ${{ steps.tag_notes.outputs.body }}
with:
script: |
const tag = '${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}';
// Pass the body via env, not a template literal: the notes contain
// backticks (the npm `install` snippet) that would otherwise break
// the JS string or have to be stripped out of the rendered page.
const { data } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tag,
name: `Glassbox ${tag}`,
body: process.env.RELEASE_BODY,
draft: true,
prerelease: false,
});
return data.id;
build:
needs: [create-release]
strategy:
fail-fast: false
matrix:
include:
- platform: macos-latest
target: aarch64-apple-darwin
- platform: macos-latest
target: x86_64-apple-darwin
- platform: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- platform: windows-latest
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install Linux dependencies
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev librsvg2-dev patchelf
- name: Install npm dependencies
run: npm ci
# The on-device Apple FM helper now ships inside the `apple-fm` dependency,
# already Developer-ID signed + notarized. build-sidecar.sh copies the
# apple-fm package (helper included) into the sidecar; the helper's embedded
# signature survives, so tauri-action's notarization of the whole bundle
# below covers it — no dedicated macOS-26 compile job is needed. See docs/22.
- name: Build sidecar
run: bash scripts/build-sidecar.sh ${{ matrix.target }}
shell: bash
- name: Build Tauri app and upload to release
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
releaseId: ${{ needs.create-release.outputs.release_id }}
args: --target ${{ matrix.target }}
# Rename the user-facing installer assets to human-friendly names. Runs once,
# after every platform shard has uploaded its bundles, so the rename is
# race-free (a single job touching each asset exactly once). The friendly
# names come from scripts/release/release-assets.mjs — the same module that
# generates the download links in the release body — so the links and the
# asset names can never drift apart. Only .dmg files are renamed; the
# updater's latest.json references the other installers by their original
# filenames.
rename-assets:
needs: [create-release, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Rename user-facing download assets to friendly names
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
with:
script: |
const { pathToFileURL } = require('node:url');
const path = require('node:path');
const modPath = path.join(process.env.GITHUB_WORKSPACE, 'scripts/release/release-assets.mjs');
const { friendlyName } = await import(pathToFileURL(modPath).href);
const releaseId = parseInt(process.env.RELEASE_ID);
const { data: assets } = await github.rest.repos.listReleaseAssets({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId,
per_page: 100,
});
for (const asset of assets) {
const newName = friendlyName(asset.name);
if (newName !== asset.name) {
console.log(`Renaming: ${asset.name} -> ${newName}`);
await github.rest.repos.updateReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
asset_id: asset.id,
name: newName,
});
}
}
# GB-779 — atomic flip of the release from draft → published.
#
# Final job in the pipeline. Runs only after every platform build has
# uploaded its bundles AND the `rename-assets` job has renamed the .dmg
# files to the friendly names the release notes link to. At this point the
# draft release has all of its assets in place, so flipping `draft:false`
# is the single atomic moment when:
# - The release page becomes publicly visible at /releases/tag/<tag>
# (every embedded download link resolves immediately — no broken-link
# window).
# - GitHub's `releases/latest` pointer moves to this release.
# - The Tauri updater starts seeing the new `latest.json`
# (`src-tauri/tauri.conf.json` endpoint).
#
# `make_latest: 'true'` is the API equivalent of GitHub's "Set as the
# latest release" checkbox — explicit so the flip can't be silently
# downgraded by repo-level settings.
publish-release:
needs: [create-release, build, rename-assets]
runs-on: ubuntu-latest
steps:
- name: Flip release from draft → published
uses: actions/github-script@v7
with:
script: |
const releaseId = ${{ needs.create-release.outputs.release_id }};
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId,
draft: false,
make_latest: 'true',
});
console.log(`Release ${releaseId} flipped from draft → published`);