Skip to content

chore: release Stone Desktop v0.8.1 #3

chore: release Stone Desktop v0.8.1

chore: release Stone Desktop v0.8.1 #3

Workflow file for this run

name: Release desktop apps
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
release_tag:
description: Existing or new release tag, for example v0.7.1
required: true
type: string
prerelease:
description: Mark the GitHub Release as a prerelease
required: false
default: false
type: boolean
permissions:
contents: read
concurrency:
group: stone-release-${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }}
cancel-in-progress: false
env:
NODE_VERSION: '24'
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }}
jobs:
quality:
name: Quality gate
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- name: Install dependencies
run: npm ci
- name: Validate release tag
run: npm run release:check -- "$RELEASE_TAG"
- name: Run checks and production build
run: npm run check
package:
name: Package ${{ matrix.name }}
needs: quality
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- name: Windows x64
id: windows-x64
os: windows-latest
builder_args: --win nsis portable --x64
artifact_path: |
release/Stone-*-windows-x64-setup.exe
release/Stone-*-windows-x64-setup.exe.blockmap
release/Stone-*-windows-x64-portable.exe
release/latest.yml
- name: Linux x64
id: linux-x64
os: ubuntu-latest
builder_args: --linux AppImage deb --x64
artifact_path: |
release/Stone-*-linux-x86_64.AppImage
release/Stone-*-linux-amd64.deb
release/latest-linux.yml
- name: Linux arm64
id: linux-arm64
os: ubuntu-24.04-arm
builder_args: --linux AppImage deb --arm64
artifact_path: |
release/Stone-*-linux-arm64.AppImage
release/Stone-*-linux-arm64.deb
release/latest-linux-arm64.yml
- name: macOS Intel and Apple Silicon
id: macos
os: macos-15
builder_args: --mac dmg zip --x64 --arm64 --config.mac.identity=- --config.mac.hardenedRuntime=false
artifact_path: |
release/Stone-*-macos-x64.dmg
release/Stone-*-macos-x64.dmg.blockmap
release/Stone-*-macos-x64.zip
release/Stone-*-macos-x64.zip.blockmap
release/Stone-*-macos-arm64.dmg
release/Stone-*-macos-arm64.dmg.blockmap
release/Stone-*-macos-arm64.zip
release/Stone-*-macos-arm64.zip.blockmap
release/latest-mac.yml
env:
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Build native packages
run: >-
npx electron-builder ${{ matrix.builder_args }} --publish never
--config.extraMetadata.homepage=https://github.com/${{ github.repository }}
--config.extraMetadata.repository=https://github.com/${{ github.repository }}
--config.extraMetadata.author.name=${{ github.repository_owner }}
--config.extraMetadata.author.email=${{ github.repository_owner }}@users.noreply.github.com
--config.linux.maintainer=${{ github.repository_owner }}@users.noreply.github.com
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: stone-${{ matrix.id }}
path: ${{ matrix.artifact_path }}
if-no-files-found: error
compression-level: 0
retention-days: 7
release:
name: Publish GitHub Release
needs: package
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
PRERELEASE_INPUT: ${{ inputs.prerelease || false }}
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- name: Install metadata validation dependencies
run: npm ci
- name: Download platform packages
uses: actions/download-artifact@v4
with:
pattern: stone-*
path: release-assets
merge-multiple: true
- name: Verify expected updater assets
shell: bash
run: |
version="${RELEASE_TAG#v}"
expected=(
"Stone-${version}-windows-x64-setup.exe"
"Stone-${version}-windows-x64-setup.exe.blockmap"
"Stone-${version}-windows-x64-portable.exe"
"Stone-${version}-linux-x86_64.AppImage"
"Stone-${version}-linux-amd64.deb"
"Stone-${version}-linux-arm64.AppImage"
"Stone-${version}-linux-arm64.deb"
"Stone-${version}-macos-x64.dmg"
"Stone-${version}-macos-x64.dmg.blockmap"
"Stone-${version}-macos-x64.zip"
"Stone-${version}-macos-x64.zip.blockmap"
"Stone-${version}-macos-arm64.dmg"
"Stone-${version}-macos-arm64.dmg.blockmap"
"Stone-${version}-macos-arm64.zip"
"Stone-${version}-macos-arm64.zip.blockmap"
"latest.yml"
"latest-mac.yml"
"latest-linux.yml"
"latest-linux-arm64.yml"
)
for asset in "${expected[@]}"; do
if [[ ! -f "release-assets/${asset}" ]]; then
echo "Missing expected release asset: ${asset}" >&2
exit 1
fi
done
if [[ "$(find release-assets -maxdepth 1 -type f | wc -l)" -ne "${#expected[@]}" ]]; then
echo 'Unexpected files were found in release-assets.' >&2
find release-assets -maxdepth 1 -type f -print >&2
exit 1
fi
- name: Validate update metadata URLs and SHA-512 hashes
shell: bash
run: |
node --input-type=module <<'NODE'
import { createHash } from 'node:crypto'
import { readFileSync, statSync } from 'node:fs'
import { join } from 'node:path'
import { load } from 'js-yaml'
const root = 'release-assets'
const version = process.env.RELEASE_TAG.replace(/^v/, '')
const manifests = {
'latest.yml': [
`Stone-${version}-windows-x64-setup.exe`,
],
'latest-mac.yml': [
`Stone-${version}-macos-x64.dmg`,
`Stone-${version}-macos-x64.zip`,
`Stone-${version}-macos-arm64.dmg`,
`Stone-${version}-macos-arm64.zip`,
],
'latest-linux.yml': [
`Stone-${version}-linux-x86_64.AppImage`,
`Stone-${version}-linux-amd64.deb`,
],
'latest-linux-arm64.yml': [
`Stone-${version}-linux-arm64.AppImage`,
`Stone-${version}-linux-arm64.deb`,
],
}
const sha512 = (file) => createHash('sha512').update(readFileSync(file)).digest('base64')
for (const [manifestName, expectedUrls] of Object.entries(manifests)) {
const manifest = load(readFileSync(join(root, manifestName), 'utf8'))
if (!manifest || manifest.version !== version || !Array.isArray(manifest.files)) {
throw new Error(`${manifestName} has an invalid version or files list.`)
}
const files = new Map(manifest.files.map((entry) => [entry.url, entry]))
if (files.size !== expectedUrls.length || expectedUrls.some((url) => !files.has(url))) {
throw new Error(`${manifestName} does not contain exactly the expected update URLs.`)
}
for (const url of expectedUrls) {
const assetPath = join(root, url)
const entry = files.get(url)
if (entry.sha512 !== sha512(assetPath)) {
throw new Error(`${manifestName} has an invalid SHA-512 hash for ${url}.`)
}
if (entry.size != null && entry.size !== statSync(assetPath).size) {
throw new Error(`${manifestName} has an invalid size for ${url}.`)
}
}
if (!files.has(manifest.path) || manifest.sha512 !== files.get(manifest.path).sha512) {
throw new Error(`${manifestName} has inconsistent legacy path or SHA-512 fields.`)
}
}
NODE
- name: Generate SHA-256 checksums
shell: bash
run: |
cd release-assets
find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%f\n' \
| LC_ALL=C sort \
| xargs sha256sum > SHA256SUMS
- name: Create draft, upload assets, and publish GitHub Release
shell: bash
run: |
assets=(release-assets/*)
release_state="$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq .isDraft 2>/dev/null || true)"
if [[ "$release_state" == 'false' ]]; then
echo "Release ${RELEASE_TAG} is already published; refusing to replace immutable updater assets." >&2
exit 1
fi
is_prerelease=false
if [[ "$RELEASE_TAG" == *-* || "$PRERELEASE_INPUT" == 'true' ]]; then
is_prerelease=true
fi
if [[ "$release_state" != 'true' ]]; then
create_args=(
"$RELEASE_TAG"
--draft
--repo "$GITHUB_REPOSITORY"
--target "$GITHUB_SHA"
--title "Stone ${RELEASE_TAG}"
--generate-notes
--notes $'> [!WARNING]\n> macOS artifacts are ad-hoc signed and are not Apple-notarized. Windows artifacts are unsigned. Linux credential storage requires a Secret Service-compatible keyring; AppImage may require FUSE 2.'
)
if [[ "$is_prerelease" == 'true' ]]; then
create_args+=(--prerelease)
fi
gh release create "${create_args[@]}"
fi
gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber --repo "$GITHUB_REPOSITORY"
mapfile -t local_assets < <(find release-assets -maxdepth 1 -type f -printf '%f\n' | LC_ALL=C sort)
mapfile -t remote_assets < <(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' | LC_ALL=C sort)
if [[ "${local_assets[*]}" != "${remote_assets[*]}" ]]; then
echo 'Draft Release assets do not match the locally verified set.' >&2
diff <(printf '%s\n' "${local_assets[@]}") <(printf '%s\n' "${remote_assets[@]}") >&2 || true
exit 1
fi
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false --prerelease="$is_prerelease"