chore: release Stone Desktop v0.8.1 #3
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |