speed up release workflow (#584)

This commit is contained in:
Neil 2026-04-13 00:25:30 -07:00 committed by GitHub
parent d3748e1c82
commit b2c31b6536
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 6 additions and 361 deletions

View file

@ -27,28 +27,13 @@ jobs:
include:
- os: macos-15
platform: mac
arch: arm64
artifact_name: release-mac-arm64
build_command: pnpm build:release
package_command: ORCA_MAC_RELEASE=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --mac --arm64 --publish never
- os: macos-15-intel
platform: mac
arch: x64
artifact_name: release-mac-x64
build_command: pnpm build:release
package_command: ORCA_MAC_RELEASE=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --mac --x64 --publish never
release_command: ORCA_MAC_RELEASE=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --mac --publish always
- os: windows-latest
platform: win
arch: x64
artifact_name: release-win-x64
build_command: pnpm build:release
package_command: pnpm exec electron-builder --config config/electron-builder.config.cjs --win --publish never
release_command: pnpm exec electron-builder --config config/electron-builder.config.cjs --win --publish always
- os: ubuntu-latest
platform: linux
arch: x64
artifact_name: release-linux-x64
build_command: pnpm build:release
package_command: pnpm exec electron-builder --config config/electron-builder.config.cjs --linux --publish never
release_command: pnpm exec electron-builder --config config/electron-builder.config.cjs --linux --publish always
runs-on: ${{ matrix.os }}
@ -83,10 +68,10 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Build app
run: ${{ matrix.build_command }}
run: pnpm build:release
- name: Package release artifacts
run: ${{ matrix.package_command }}
- name: Publish release artifacts
run: ${{ matrix.release_command }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.MAC_CERTS }}
@ -95,81 +80,12 @@ jobs:
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Collect release assets
shell: bash
run: |
set -euo pipefail
mkdir -p release-publish
shopt -s nullglob
for pattern in \
"dist/*.dmg" \
"dist/*.zip" \
"dist/*.AppImage" \
"dist/*.deb" \
"dist/*.exe" \
"dist/*.blockmap" \
"dist/latest*.yml"; do
for file in $pattern; do
cp "$file" release-publish/
done
done
# Why: each split macOS build emits a same-named latest-mac.yml into
# dist/. We keep the arm64 manifest as canonical and rename the Intel
# one so the publish job can merge both manifests before GitHub sees
# the assets.
if [[ "${{ matrix.platform }}" == "mac" && "${{ matrix.arch }}" == "x64" ]]; then
mv release-publish/latest-mac.yml release-publish/latest-mac-x64.yml
fi
- name: Upload release assets
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: release-publish/*
if-no-files-found: error
publish-release:
needs: release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Download release assets
uses: actions/download-artifact@v4
with:
pattern: release-*
path: release-assets
- name: Merge macOS updater manifests
run: |
node config/scripts/merge-mac-update-manifests.mjs \
release-assets/release-mac-arm64/latest-mac.yml \
release-assets/release-mac-x64/latest-mac-x64.yml
rm release-assets/release-mac-x64/latest-mac-x64.yml
- name: Upload release assets
shell: bash
run: |
set -euo pipefail
files=()
while IFS= read -r -d '' file; do
files+=("$file")
done < <(find release-assets -type f -print0)
gh release upload "${{ github.ref_name }}" "${files[@]}" --clobber --repo "${{ github.repository }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish release
run: gh release edit ${{ github.ref_name }} --draft=false --repo ${{ github.repository }}
env:

View file

@ -1,271 +0,0 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from 'node:fs'
function stripSingleQuotes(value) {
if (value.startsWith("'") && value.endsWith("'")) {
return value.slice(1, -1).replace(/''/g, "'")
}
return value
}
function parseScalarValue(rawValue) {
const trimmed = rawValue.trim()
const isQuoted = trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2
const value = isQuoted ? trimmed.slice(1, -1).replace(/''/g, "'") : trimmed
if (isQuoted) {
return value
}
if (value === 'true') {
return true
}
if (value === 'false') {
return false
}
if (/^-?\d+(?:\.\d+)?$/.test(value)) {
return Number(value)
}
return value
}
function finalizeFileRecord(currentFile, sourcePath, lineNumber) {
if (currentFile == null) {
return null
}
if (
typeof currentFile.url !== 'string' ||
typeof currentFile.sha512 !== 'string' ||
typeof currentFile.size !== 'number'
) {
throw new Error(
`Invalid macOS update manifest at ${sourcePath}:${lineNumber}: incomplete file entry.`
)
}
return {
url: currentFile.url,
sha512: currentFile.sha512,
size: currentFile.size
}
}
function parseMacUpdateManifest(raw, sourcePath) {
const lines = raw.split(/\r?\n/)
const files = []
const extras = {}
let version = null
let releaseDate = null
let inFiles = false
let currentFile = null
for (const [index, rawLine] of lines.entries()) {
const lineNumber = index + 1
const line = rawLine.trimEnd()
if (line.length === 0) {
continue
}
const fileUrlMatch = line.match(/^ - url:\s*(.+)$/)
if (fileUrlMatch?.[1]) {
const finalized = finalizeFileRecord(currentFile, sourcePath, lineNumber)
if (finalized) {
files.push(finalized)
}
currentFile = { url: stripSingleQuotes(fileUrlMatch[1].trim()) }
inFiles = true
continue
}
const fileShaMatch = line.match(/^ sha512:\s*(.+)$/)
if (fileShaMatch?.[1]) {
if (currentFile == null) {
throw new Error(
`Invalid macOS update manifest at ${sourcePath}:${lineNumber}: sha512 without a file entry.`
)
}
currentFile.sha512 = stripSingleQuotes(fileShaMatch[1].trim())
continue
}
const fileSizeMatch = line.match(/^ size:\s*(\d+)$/)
if (fileSizeMatch?.[1]) {
if (currentFile == null) {
throw new Error(
`Invalid macOS update manifest at ${sourcePath}:${lineNumber}: size without a file entry.`
)
}
currentFile.size = Number(fileSizeMatch[1])
continue
}
if (line === 'files:') {
inFiles = true
continue
}
if (inFiles && currentFile != null) {
const finalized = finalizeFileRecord(currentFile, sourcePath, lineNumber)
if (finalized) {
files.push(finalized)
}
currentFile = null
}
inFiles = false
const topLevelMatch = line.match(/^([A-Za-z][A-Za-z0-9]*):\s*(.+)$/)
if (!topLevelMatch?.[1] || topLevelMatch[2] === undefined) {
throw new Error(
`Invalid macOS update manifest at ${sourcePath}:${lineNumber}: unsupported line '${line}'.`
)
}
const [, key, rawValue] = topLevelMatch
const value = parseScalarValue(rawValue)
if (key === 'version') {
if (typeof value !== 'string') {
throw new Error(
`Invalid macOS update manifest at ${sourcePath}:${lineNumber}: version must be a string.`
)
}
version = value
continue
}
if (key === 'releaseDate') {
if (typeof value !== 'string') {
throw new Error(
`Invalid macOS update manifest at ${sourcePath}:${lineNumber}: releaseDate must be a string.`
)
}
releaseDate = value
continue
}
if (key === 'path' || key === 'sha512') {
// Why: electron-updater only needs the expanded files[] entries to decide
// which mac ZIP to fetch for the current architecture. We intentionally
// drop the single-file legacy fields because keeping one arch-specific
// path here would make the merged manifest lie about the other arch.
continue
}
extras[key] = value
}
const finalized = finalizeFileRecord(currentFile, sourcePath, lines.length)
if (finalized) {
files.push(finalized)
}
if (!version) {
throw new Error(`Invalid macOS update manifest at ${sourcePath}: missing version.`)
}
if (!releaseDate) {
throw new Error(`Invalid macOS update manifest at ${sourcePath}: missing releaseDate.`)
}
if (files.length === 0) {
throw new Error(`Invalid macOS update manifest at ${sourcePath}: missing files.`)
}
return {
version,
releaseDate,
files,
extras
}
}
function mergeExtras(primary, secondary) {
const merged = { ...primary }
for (const [key, value] of Object.entries(secondary)) {
const existing = merged[key]
if (existing !== undefined && existing !== value) {
throw new Error(
`Cannot merge macOS update manifests: conflicting '${key}' values ('${existing}' vs '${value}').`
)
}
merged[key] = value
}
return merged
}
function mergeMacUpdateManifests(primary, secondary) {
if (primary.version !== secondary.version) {
throw new Error(
`Cannot merge macOS update manifests with different versions (${primary.version} vs ${secondary.version}).`
)
}
const filesByUrl = new Map()
for (const file of [...primary.files, ...secondary.files]) {
const existing = filesByUrl.get(file.url)
if (existing && (existing.sha512 !== file.sha512 || existing.size !== file.size)) {
throw new Error(
`Cannot merge macOS update manifests: conflicting file entry for ${file.url}.`
)
}
filesByUrl.set(file.url, file)
}
return {
version: primary.version,
releaseDate:
primary.releaseDate >= secondary.releaseDate ? primary.releaseDate : secondary.releaseDate,
files: [...filesByUrl.values()],
extras: mergeExtras(primary.extras, secondary.extras)
}
}
function quoteYamlString(value) {
return `'${value.replace(/'/g, "''")}'`
}
function serializeScalarValue(value) {
if (typeof value === 'string') {
return quoteYamlString(value)
}
return String(value)
}
function serializeMacUpdateManifest(manifest) {
const lines = [`version: ${manifest.version}`, 'files:']
for (const file of manifest.files) {
lines.push(` - url: ${file.url}`)
lines.push(` sha512: ${file.sha512}`)
lines.push(` size: ${file.size}`)
}
for (const [key, value] of Object.entries(manifest.extras)) {
lines.push(`${key}: ${serializeScalarValue(value)}`)
}
lines.push(`releaseDate: ${quoteYamlString(manifest.releaseDate)}`)
lines.push('')
return lines.join('\n')
}
function main() {
const [primaryPath, secondaryPath, outputPath = primaryPath] = process.argv.slice(2)
if (!primaryPath || !secondaryPath) {
console.error(
'Usage: node config/scripts/merge-mac-update-manifests.mjs <latest-mac.yml> <latest-mac-x64.yml> [output-path]'
)
process.exit(1)
}
const primary = parseMacUpdateManifest(readFileSync(primaryPath, 'utf8'), primaryPath)
const secondary = parseMacUpdateManifest(readFileSync(secondaryPath, 'utf8'), secondaryPath)
const merged = mergeMacUpdateManifests(primary, secondary)
writeFileSync(outputPath, serializeMacUpdateManifest(merged))
}
main()