Skip to content

v2.40.0 — Tiger Studio in the browser (tigersystem.io/studio) with on… #182

v2.40.0 — Tiger Studio in the browser (tigersystem.io/studio) with on…

v2.40.0 — Tiger Studio in the browser (tigersystem.io/studio) with on… #182

Workflow file for this run

name: Build & Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
# ─────────────────────────────────────────────────────────────────
# Runs FIRST — creates the draft release for this tag and fills its
# body with the CHANGELOG.md section, BEFORE any platform build runs.
# The three build jobs then just attach their assets to this already-
# described draft, so none of them races to write (or accidentally
# blank) the release body.
# ─────────────────────────────────────────────────────────────────
prepare-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Build release notes from CHANGELOG.md
run: node scripts/extract-changelog.mjs "${{ github.ref_name }}" > release-notes.md
# Release NAME is the version without the leading "v" (e.g. tag v2.0.0 → "2.0.0"),
# matching the historical naming. softprops defaults the name to the tag otherwise.
- name: Release version (strip leading v)
id: ver
run: echo "num=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Create draft release with description
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: ${{ steps.ver.outputs.num }}
draft: true
body_path: release-notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
needs: prepare-release
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
platform: mac
- os: windows-latest
platform: win
- os: ubuntu-latest
platform: linux
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 24
- name: Install Linux system dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update -y
sudo apt-get install -y \
libpcsclite-dev \
libusb-1.0-0-dev \
libudev-dev \
build-essential \
python3 \
rpm \
fakeroot
- run: npm ci
# --- macOS only: decode the App Store Connect API Key (.p8) into a file so notarytool can read it ---
- name: Decode App Store Connect API Key (.p8)
if: matrix.os == 'macos-latest'
env:
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
run: |
mkdir -p ~/private_keys
echo -n "$APPLE_API_KEY_BASE64" | base64 --decode > ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8
chmod 600 ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8
# Export the resolved path for the next step
echo "APPLE_API_KEY=$HOME/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8" >> "$GITHUB_ENV"
# ─────────────────────────────────────────────────────────────────
# macOS signing keychain — built HERE rather than by electron-builder.
#
# Its own path passes the certificate's IMPORT password to
# `security set-key-partition-list -k`, but that flag authenticates
# against the KEYCHAIN, not against the .p12. The two are different
# secrets, and macOS used not to check — the runner image that ships
# macOS 26 does, so signing began failing with
# security: SecKeychainUnlock: The user name or passphrase you
# entered is not correct
# on 2026-09-12, with nothing changed on our side: same certificate
# (valid to 2031), same secrets (untouched since May), same
# electron-builder (26.15.3). Only the image moved, 20260728 → 20260907.
#
# Upstream fixed it in PR #10101, but that sits in 27.0.0-alpha.8 and is
# in NO stable release — 26.15.7 and 26.16.1 both lack it. So we own the
# keychain instead of waiting: create it, import the cert, and authorise
# codesign with the keychain's OWN password. `CSC_KEYCHAIN` then tells
# electron-builder to use it and skip the broken path entirely.
#
# The keychain password is generated per run and never leaves the runner.
# Delete this step once the fix lands in a stable electron-builder and
# the dependency is bumped — not before.
# ─────────────────────────────────────────────────────────────────
- name: Prepare macOS signing keychain
if: matrix.os == 'macos-latest'
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
run: |
set -euo pipefail
KEYCHAIN="$RUNNER_TEMP/tigertag-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 24)"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
echo "$MACOS_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" \
-P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
rm -f "$RUNNER_TEMP/cert.p12"
# The keychain's OWN password — this is the line electron-builder gets wrong.
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" > /dev/null
# Put it in the search list so codesign can find the identity.
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
echo "CSC_KEYCHAIN=$KEYCHAIN" >> "$GITHUB_ENV"
echo "Identities available for signing:"
security find-identity -v -p codesigning "$KEYCHAIN" | sed 's/[A-F0-9]\{40\}/<hash>/'
# ─────────────────────────────────────────────────────────────────
# macOS / Linux — build & publish in one step (no post-build signing).
# Windows is handled in the next 4 steps so we can SIGN the .exe with
# Microsoft Trusted Signing (Azure) BEFORE publishing it to the Release.
# ─────────────────────────────────────────────────────────────────
- name: Build & publish (macOS / Linux)
if: matrix.os != 'windows-latest'
run: npm run build:${{ matrix.platform }}
env:
# GitHub Releases publish (electron-builder reads GH_TOKEN automatically)
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# macOS code signing — the identity lives in the keychain prepared above
# (CSC_KEYCHAIN, exported to GITHUB_ENV). CSC_LINK is deliberately NOT
# set: passing it makes electron-builder build its own keychain again
# and take the path this works around.
CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
# macOS notarization via App Store Connect API Key (preferred over Apple ID + password)
# APPLE_API_KEY is set in the previous step via GITHUB_ENV (path to decoded .p8)
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# Google Desktop OAuth — Touch ID / loopback sign-in
TIGERTAG_GOOGLE_DESKTOP_CLIENT_ID: ${{ secrets.TIGERTAG_GOOGLE_DESKTOP_CLIENT_ID }}
TIGERTAG_GOOGLE_DESKTOP_CLIENT_SECRET: ${{ secrets.TIGERTAG_GOOGLE_DESKTOP_CLIENT_SECRET }}
# ─────────────────────────────────────────────────────────────────
# Windows path — split into:
# 1. Build .exe locally with --publish never (so we can sign first)
# 2. Check whether Trusted Signing secrets are configured in repo
# 3. Sign .exe artifacts via Microsoft Trusted Signing (skipped
# gracefully when secrets are absent → unsigned build still ships)
# 4. Upload .exe (+ .blockmap + latest.yml) to the GitHub Release
# ─────────────────────────────────────────────────────────────────
- name: Build Windows (no publish — sign first, publish after)
if: matrix.os == 'windows-latest'
run: npm run build:win:nopublish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Google Desktop OAuth — Touch ID / loopback sign-in
TIGERTAG_GOOGLE_DESKTOP_CLIENT_ID: ${{ secrets.TIGERTAG_GOOGLE_DESKTOP_CLIENT_ID }}
TIGERTAG_GOOGLE_DESKTOP_CLIENT_SECRET: ${{ secrets.TIGERTAG_GOOGLE_DESKTOP_CLIENT_SECRET }}
- name: Check Trusted Signing secrets availability
id: check_signing
if: matrix.os == 'windows-latest'
shell: bash
env:
AZ_TENANT: ${{ secrets.AZURE_TENANT_ID }}
AZ_CLIENT: ${{ secrets.AZURE_CLIENT_ID }}
AZ_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
CERT_PROFILE: ${{ vars.TRUSTED_SIGNING_CERT_PROFILE }}
run: |
if [ -n "$AZ_TENANT" ] && [ -n "$AZ_CLIENT" ] && [ -n "$AZ_SECRET" ] && [ -n "$CERT_PROFILE" ]; then
echo "ready=true" >> "$GITHUB_OUTPUT"
echo "✓ Trusted Signing secrets present — Windows artifacts will be signed under cert profile '$CERT_PROFILE'."
else
echo "ready=false" >> "$GITHUB_OUTPUT"
echo "⚠ Trusted Signing secrets / cert profile missing — Windows artifacts will be UNSIGNED for this run."
echo " Configure secrets AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CLIENT_SECRET and repo variable TRUSTED_SIGNING_CERT_PROFILE to enable signing."
fi
- name: Sign Windows artifacts (Microsoft Trusted Signing)
if: matrix.os == 'windows-latest' && steps.check_signing.outputs.ready == 'true'
uses: azure/trusted-signing-action@v0.5.1
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: https://neu.codesigning.azure.net/
trusted-signing-account-name: TigerTagStudioSigning
certificate-profile-name: ${{ vars.TRUSTED_SIGNING_CERT_PROFILE }}
files-folder: dist
files-folder-filter: exe
files-folder-recurse: false
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
# Signing changes the .exe (the authenticode signature is embedded into
# the PE binary), which invalidates the SHA-512 + size that
# electron-builder wrote into dist/latest.yml. Without this regen step,
# auto-updater clients would download the new signed .exe, hash-check it
# against the stale latest.yml, mismatch, and refuse the update.
- name: Regenerate dist/latest.yml after signing
if: matrix.os == 'windows-latest' && steps.check_signing.outputs.ready == 'true'
run: node scripts/regen-latest-yml.mjs
# Windows ships TWO .exe (NSIS installer + portable). electron-builder only
# writes latest.yml for `nsis` targets (isSuitableWindowsTarget in
# PublishManager), so the portable can never become the auto-update
# target — but that is a library invariant, not ours. Assert it here so a
# future target rename or an electron-builder change cannot silently point
# every Windows user's updater at a build that does not install.
- name: Assert latest.yml never points at the portable build
if: matrix.os == 'windows-latest'
shell: bash
run: |
if grep -qi "portable" dist/latest.yml; then
echo "::error::dist/latest.yml references the portable build — the auto-updater would download an app that does not install."
cat dist/latest.yml
exit 1
fi
echo "✓ latest.yml points only at the installer:"
grep -E "^(path| - url):" dist/latest.yml
- name: Release version (strip leading v)
if: matrix.os == 'windows-latest'
id: verwin
shell: bash # windows runner defaults to PowerShell; force bash for ${VAR#v}
run: echo "num=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Publish Windows artifacts to GitHub Release
if: matrix.os == 'windows-latest'
# v3.0.0 (2026-04-12) moved the action runtime from Node 20 to
# Node 24, silencing the GitHub Actions deprecation warning that
# v2.6.2 emits. Same inputs/outputs, no breaking API changes.
#
# `draft: true` matches the `releaseType: "draft"` set in
# package.json so electron-builder (mac/linux) and softprops
# (windows) agree on the same release type. With a mismatch,
# the second tool to fire either skips its uploads or creates
# a duplicate release. The release stays in draft until manually
# promoted via the GitHub UI — that's how we want it: lets us
# verify all artifacts are present + spot a broken build
# before the auto-updater picks the new version.
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: ${{ steps.verwin.outputs.num }}
draft: true
files: |
dist/*.exe
dist/*.exe.blockmap
dist/latest.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}