mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-24 21:41:08 +02:00
Wire windowsSign (packager + Squirrel maker) through Azure Trusted Signing's signtool dlib. Signing activates only when the CI env vars are present; local and mac/linux builds are unaffected. The Windows job stages the dlib, metadata.json, and a modern SDK signtool under C:\azsign (space-free paths — @electron/windows-sign splits signWithParams on spaces). Requires repo secrets: AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_ENDPOINT, AZURE_CODE_SIGNING_NAME, AZURE_CERT_PROFILE_NAME. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
297 lines
11 KiB
YAML
297 lines
11 KiB
YAML
name: Build Electron App
|
|
|
|
on:
|
|
release:
|
|
types: [published]
|
|
|
|
permissions:
|
|
contents: write # Required to upload release assets
|
|
|
|
jobs:
|
|
build-macos:
|
|
runs-on: macos-latest
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 10
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24.15.0
|
|
cache: 'pnpm'
|
|
cache-dependency-path: 'apps/x/pnpm-lock.yaml'
|
|
|
|
- name: Extract version from tag
|
|
id: version
|
|
run: |
|
|
VERSION="${GITHUB_REF#refs/tags/v}"
|
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
echo "Extracted version: ${VERSION}"
|
|
|
|
- name: Update package.json versions
|
|
run: |
|
|
node -e "
|
|
const fs = require('fs');
|
|
const version = '${{ steps.version.outputs.version }}';
|
|
|
|
// Update apps/x/package.json
|
|
const rootPackage = JSON.parse(fs.readFileSync('apps/x/package.json', 'utf8'));
|
|
rootPackage.version = version;
|
|
fs.writeFileSync('apps/x/package.json', JSON.stringify(rootPackage, null, 2) + '\n');
|
|
|
|
// Update apps/x/apps/main/package.json
|
|
const mainPackage = JSON.parse(fs.readFileSync('apps/x/apps/main/package.json', 'utf8'));
|
|
mainPackage.version = version;
|
|
fs.writeFileSync('apps/x/apps/main/package.json', JSON.stringify(mainPackage, null, 2) + '\n');
|
|
|
|
console.log('Updated version to:', version);
|
|
"
|
|
|
|
- name: Import Code Signing Certificate
|
|
env:
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
run: |
|
|
# Create a temporary keychain
|
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
|
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
|
|
|
# Create keychain
|
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
|
|
# Decode and import certificate
|
|
echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
|
|
security import $RUNNER_TEMP/certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
|
|
|
|
# Allow codesign to access the keychain
|
|
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
|
|
# Add keychain to search list
|
|
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain
|
|
|
|
# Verify certificate was imported
|
|
security find-identity -v "$KEYCHAIN_PATH"
|
|
|
|
# Clean up certificate file
|
|
rm -f $RUNNER_TEMP/certificate.p12
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
working-directory: apps/x
|
|
|
|
- name: Build electron app
|
|
env:
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
VITE_PUBLIC_POSTHOG_KEY: ${{ secrets.VITE_PUBLIC_POSTHOG_KEY }}
|
|
VITE_PUBLIC_POSTHOG_HOST: ${{ secrets.VITE_PUBLIC_POSTHOG_HOST }}
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: npx electron-forge publish --arch=arm64,x64 --platform=darwin
|
|
working-directory: apps/x/apps/main
|
|
|
|
- name: Cleanup keychain
|
|
if: always()
|
|
run: |
|
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
|
if [ -f "$KEYCHAIN_PATH" ]; then
|
|
security delete-keychain "$KEYCHAIN_PATH" || true
|
|
fi
|
|
|
|
- name: Upload workflow artifacts
|
|
uses: actions/upload-artifact@v6
|
|
with:
|
|
name: distributables
|
|
path: apps/x/apps/main/out/make/*
|
|
if-no-files-found: error
|
|
retention-days: 30
|
|
|
|
build-linux:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 10
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24.15.0
|
|
cache: 'pnpm'
|
|
cache-dependency-path: 'apps/x/pnpm-lock.yaml'
|
|
|
|
- name: Extract version from tag
|
|
id: version
|
|
run: |
|
|
VERSION="${GITHUB_REF#refs/tags/v}"
|
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
echo "Extracted version: ${VERSION}"
|
|
|
|
- name: Update package.json versions
|
|
run: |
|
|
node -e "
|
|
const fs = require('fs');
|
|
const version = '${{ steps.version.outputs.version }}';
|
|
|
|
// Update apps/x/package.json
|
|
const rootPackage = JSON.parse(fs.readFileSync('apps/x/package.json', 'utf8'));
|
|
rootPackage.version = version;
|
|
fs.writeFileSync('apps/x/package.json', JSON.stringify(rootPackage, null, 2) + '\n');
|
|
|
|
// Update apps/x/apps/main/package.json
|
|
const mainPackage = JSON.parse(fs.readFileSync('apps/x/apps/main/package.json', 'utf8'));
|
|
mainPackage.version = version;
|
|
fs.writeFileSync('apps/x/apps/main/package.json', JSON.stringify(mainPackage, null, 2) + '\n');
|
|
|
|
console.log('Updated version to:', version);
|
|
"
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
working-directory: apps/x
|
|
|
|
- name: Build node-pty native binary for Linux
|
|
working-directory: apps/x
|
|
run: |
|
|
# node-pty ships prebuilt binaries only for darwin/win32; compile the
|
|
# linux-x64 binary so bundle.mjs can stage it into the package. Without
|
|
# this the Linux app crashes on launch (missing prebuilds/linux-x64/pty.node).
|
|
PTY="node_modules/.pnpm/node-pty@1.1.0/node_modules/node-pty"
|
|
cd "$PTY"
|
|
npx node-gyp rebuild
|
|
mkdir -p prebuilds/linux-x64
|
|
cp build/Release/pty.node prebuilds/linux-x64/
|
|
|
|
- name: Build electron app
|
|
env:
|
|
VITE_PUBLIC_POSTHOG_KEY: ${{ secrets.VITE_PUBLIC_POSTHOG_KEY }}
|
|
VITE_PUBLIC_POSTHOG_HOST: ${{ secrets.VITE_PUBLIC_POSTHOG_HOST }}
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
ROWBOAT_SKIP_PACMAN: '1' # Arch Linux package is built locally only, never in CI
|
|
run: npx electron-forge publish --arch=x64 --platform=linux
|
|
working-directory: apps/x/apps/main
|
|
|
|
- name: Upload workflow artifacts
|
|
uses: actions/upload-artifact@v6
|
|
with:
|
|
name: distributables-linux
|
|
path: apps/x/apps/main/out/make/*
|
|
if-no-files-found: error
|
|
retention-days: 30
|
|
|
|
build-windows:
|
|
runs-on: windows-latest
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 10
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24.15.0
|
|
cache: 'pnpm'
|
|
cache-dependency-path: 'apps/x/pnpm-lock.yaml'
|
|
|
|
- name: Extract version from tag
|
|
id: version
|
|
shell: bash
|
|
run: |
|
|
VERSION="${GITHUB_REF#refs/tags/v}"
|
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
echo "Extracted version: ${VERSION}"
|
|
|
|
- name: Update package.json versions
|
|
shell: bash
|
|
run: |
|
|
node -e "
|
|
const fs = require('fs');
|
|
const version = '${{ steps.version.outputs.version }}';
|
|
|
|
// Update apps/x/package.json
|
|
const rootPackage = JSON.parse(fs.readFileSync('apps/x/package.json', 'utf8'));
|
|
rootPackage.version = version;
|
|
fs.writeFileSync('apps/x/package.json', JSON.stringify(rootPackage, null, 2) + '\n');
|
|
|
|
// Update apps/x/apps/main/package.json
|
|
const mainPackage = JSON.parse(fs.readFileSync('apps/x/apps/main/package.json', 'utf8'));
|
|
mainPackage.version = version;
|
|
fs.writeFileSync('apps/x/apps/main/package.json', JSON.stringify(mainPackage, null, 2) + '\n');
|
|
|
|
console.log('Updated version to:', version);
|
|
"
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
working-directory: apps/x
|
|
|
|
- name: Setup Azure Trusted Signing
|
|
shell: pwsh
|
|
run: |
|
|
# Stage everything under a path WITHOUT spaces: @electron/windows-sign
|
|
# splits signWithParams on spaces, so the /dlib and /dmdf paths must be
|
|
# space-free.
|
|
$dir = "C:\azsign"
|
|
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
|
|
# The signtool plugin that delegates signing to Azure Trusted Signing.
|
|
nuget install Microsoft.Trusted.Signing.Client -x -OutputDirectory "$dir\nuget"
|
|
$dlib = "$dir\nuget\Microsoft.Trusted.Signing.Client\bin\x64\Azure.CodeSigning.Dlib.dll"
|
|
if (-not (Test-Path $dlib)) { throw "Azure.CodeSigning.Dlib.dll not found at $dlib" }
|
|
|
|
# Tells the dlib which account/profile to sign with.
|
|
@{
|
|
Endpoint = "${{ secrets.AZURE_ENDPOINT }}"
|
|
CodeSigningAccountName = "${{ secrets.AZURE_CODE_SIGNING_NAME }}"
|
|
CertificateProfileName = "${{ secrets.AZURE_CERT_PROFILE_NAME }}"
|
|
} | ConvertTo-Json | Out-File "$dir\metadata.json" -Encoding utf8
|
|
|
|
# Newest Windows SDK signtool (the one vendored by @electron/windows-sign
|
|
# is too old to load the dlib). Copied into the space-free dir.
|
|
$signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe" |
|
|
Where-Object { $_.Directory.Parent.Name -match '^\d+(\.\d+)+$' } |
|
|
Sort-Object { [version]$_.Directory.Parent.Name } | Select-Object -Last 1
|
|
if (-not $signtool) { throw "signtool.exe not found in Windows SDK" }
|
|
Copy-Item $signtool.FullName "$dir\signtool.exe"
|
|
& "$dir\signtool.exe" | Select-Object -First 3
|
|
|
|
echo "AZURE_CODE_SIGNING_DLIB=$dlib" >> $env:GITHUB_ENV
|
|
echo "AZURE_METADATA_JSON=$dir\metadata.json" >> $env:GITHUB_ENV
|
|
echo "SIGNTOOL_PATH=$dir\signtool.exe" >> $env:GITHUB_ENV
|
|
|
|
- name: Build electron app
|
|
env:
|
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
|
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
|
VITE_PUBLIC_POSTHOG_KEY: ${{ secrets.VITE_PUBLIC_POSTHOG_KEY }}
|
|
VITE_PUBLIC_POSTHOG_HOST: ${{ secrets.VITE_PUBLIC_POSTHOG_HOST }}
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: npx electron-forge publish --arch=x64 --platform=win32
|
|
working-directory: apps/x/apps/main
|
|
|
|
- name: Upload workflow artifacts
|
|
uses: actions/upload-artifact@v6
|
|
with:
|
|
name: distributables-windows
|
|
path: apps/x/apps/main/out/make/*
|
|
if-no-files-found: error
|
|
retention-days: 30
|