mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
Speed up release workflow (#579)
* speed up release workflow * fix lint in manifest merge helper * remove release preflight gate * update CI to node 24
This commit is contained in:
parent
d7e045b19a
commit
424ceea07e
4 changed files with 367 additions and 15 deletions
2
.github/workflows/pr.yml
vendored
2
.github/workflows/pr.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
|
|
|||
108
.github/workflows/release.yml
vendored
108
.github/workflows/release.yml
vendored
|
|
@ -25,12 +25,30 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
|
|
@ -44,7 +62,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
|
@ -54,9 +72,15 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build and release (macOS)
|
||||
- name: Verify macOS signing environment
|
||||
if: matrix.platform == 'mac'
|
||||
run: pnpm build:mac:release --publish always
|
||||
run: node config/scripts/verify-macos-release-env.mjs
|
||||
|
||||
- name: Build app
|
||||
run: ${{ matrix.build_command }}
|
||||
|
||||
- name: Package release artifacts
|
||||
run: ${{ matrix.package_command }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CSC_LINK: ${{ secrets.MAC_CERTS }}
|
||||
|
|
@ -65,17 +89,39 @@ jobs:
|
|||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
- name: Build and release (Windows)
|
||||
if: matrix.platform == 'win'
|
||||
run: pnpm build:win --publish always
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Collect release assets
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p release-publish
|
||||
|
||||
- name: Build and release (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
run: pnpm build:linux --publish always
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shopt -s nullglob
|
||||
for pattern in \
|
||||
"release/*.dmg" \
|
||||
"release/*.zip" \
|
||||
"release/*.AppImage" \
|
||||
"release/*.deb" \
|
||||
"release/*.exe" \
|
||||
"release/*.blockmap" \
|
||||
"release/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. 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
|
||||
|
|
@ -83,6 +129,40 @@ jobs:
|
|||
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:
|
||||
|
|
|
|||
271
config/scripts/merge-mac-update-manifests.mjs
Normal file
271
config/scripts/merge-mac-update-manifests.mjs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
#!/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()
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
"build:cli": "tsc -p config/tsconfig.cli.json --outDir out --composite false --incremental false",
|
||||
"build:electron-vite": "node config/scripts/run-electron-vite-build.mjs",
|
||||
"build": "pnpm run typecheck && pnpm run build:electron-vite && pnpm run build:cli",
|
||||
"build:release": "pnpm run build:electron-vite && pnpm run build:cli",
|
||||
"postinstall": "pnpm rebuild electron && electron-builder install-app-deps",
|
||||
"build:unpack": "pnpm run build && electron-builder --config config/electron-builder.config.cjs --dir",
|
||||
"build:win": "pnpm run build && electron-builder --config config/electron-builder.config.cjs --win",
|
||||
|
|
|
|||
Loading…
Reference in a new issue