build: publish linux arm64 release artifacts (#4755)

This commit is contained in:
Neil 2026-06-22 01:07:16 -07:00 committed by GitHub
parent 24000561b4
commit 531bcc9a0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 164 additions and 20 deletions

View File

@ -716,8 +716,14 @@ jobs:
~\AppData\Local\electron\Cache
~\AppData\Local\electron-builder\Cache
- os: ubuntu-latest
platform: linux
release_command: node config/scripts/ensure-native-runtime.mjs --runtime=electron && pnpm exec electron-builder --config config/electron-builder.config.cjs --linux --publish always
platform: linux-x64
release_command: node config/scripts/ensure-native-runtime.mjs --runtime=electron && pnpm exec electron-builder --config config/electron-builder.config.cjs --linux AppImage deb rpm --x64 --publish always
eb_cache_path: |
~/.cache/electron
~/.cache/electron-builder
- os: ubuntu-24.04-arm
platform: linux-arm64
release_command: node config/scripts/ensure-native-runtime.mjs --runtime=electron && ORCA_LINUX_ARM64_RELEASE=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --linux AppImage deb rpm --arm64 --publish always
eb_cache_path: |
~/.cache/electron
~/.cache/electron-builder

View File

@ -9,6 +9,7 @@ const {
} = require('./packaged-runtime-node-modules.cjs')
const isMacRelease = process.env.ORCA_MAC_RELEASE === '1'
const isLinuxArm64Release = process.env.ORCA_LINUX_ARM64_RELEASE === '1'
const featureWallResources = {
from: 'resources/onboarding/feature-wall',
to: 'onboarding/feature-wall'
@ -273,12 +274,12 @@ module.exports = {
},
featureWallResources
],
target: ['AppImage', 'deb', 'rpm'],
target: ['AppImage', 'deb'],
maintainer: 'stablyai',
category: 'Utility'
},
appImage: {
artifactName: 'orca-linux.${ext}'
artifactName: isLinuxArm64Release ? 'orca-linux-arm64.${ext}' : 'orca-linux.${ext}'
},
deb: {
packageName: 'orca-ide',

View File

@ -71,8 +71,8 @@ describe('electron-builder config', () => {
expect(electronBuilderConfig.linux.desktop.entry.StartupWMClass).toBe('orca')
})
it('builds RPMs without changing existing Linux artifact names', () => {
expect(electronBuilderConfig.linux.target).toEqual(['AppImage', 'deb', 'rpm'])
it('uses AppImage and deb as local Linux targets without changing existing artifact names', () => {
expect(electronBuilderConfig.linux.target).toEqual(['AppImage', 'deb'])
expect(electronBuilderConfig.appImage.artifactName).toBe('orca-linux.${ext}')
expect(electronBuilderConfig.deb.artifactName).toBe('orca-ide_${version}_${arch}.${ext}')
expect(electronBuilderConfig.rpm).toMatchObject({
@ -81,6 +81,26 @@ describe('electron-builder config', () => {
})
})
it('uses a distinct AppImage name for Linux arm64 release uploads', () => {
const configPath = require.resolve('../electron-builder.config.cjs')
const original = process.env.ORCA_LINUX_ARM64_RELEASE
try {
delete require.cache[configPath]
process.env.ORCA_LINUX_ARM64_RELEASE = '1'
expect(require('../electron-builder.config.cjs').appImage.artifactName).toBe(
'orca-linux-arm64.${ext}'
)
} finally {
if (original === undefined) {
delete process.env.ORCA_LINUX_ARM64_RELEASE
} else {
process.env.ORCA_LINUX_ARM64_RELEASE = original
}
delete require.cache[configPath]
require('../electron-builder.config.cjs')
}
})
it('uses Orca native rebuild hook instead of electron-builder default rebuild', () => {
expect(electronBuilderConfig.beforeBuild).toBe(electronBuilderNativeRebuild)
expect(electronBuilderConfig.npmRebuild).toBe(true)

View File

@ -0,0 +1,19 @@
import os from 'node:os'
const maxOldSpaceSizeMb = 4096
const minOldSpaceSizeMb = 2048
const reservedSystemMemoryMb = 1024
export function getBuildOldSpaceSizeMb(totalMemoryBytes = os.totalmem()) {
const totalMemoryMb = Math.floor(totalMemoryBytes / 1024 / 1024)
const hostSizedLimitMb = Math.max(minOldSpaceSizeMb, totalMemoryMb - reservedSystemMemoryMb)
return Math.min(maxOldSpaceSizeMb, hostSizedLimitMb)
}
export function appendBuildOldSpaceOption(existingNodeOptions, totalMemoryBytes = os.totalmem()) {
const requestedNodeOptions = `--max-old-space-size=${getBuildOldSpaceSizeMb(totalMemoryBytes)}`
const trimmedNodeOptions = existingNodeOptions?.trim()
return trimmedNodeOptions ? `${trimmedNodeOptions} ${requestedNodeOptions}` : requestedNodeOptions
}

View File

@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest'
import { appendBuildOldSpaceOption, getBuildOldSpaceSizeMb } from './node-old-space-limit.mjs'
const gib = 1024 * 1024 * 1024
describe('node old-space build limit', () => {
it('caps larger hosts at the release-runner build heap', () => {
expect(getBuildOldSpaceSizeMb(8 * gib)).toBe(4096)
})
it('reserves memory for smaller arm64 hosts', () => {
expect(getBuildOldSpaceSizeMb(4 * gib)).toBe(3072)
})
it('keeps existing NODE_OPTIONS while appending the build heap limit last', () => {
expect(appendBuildOldSpaceOption('--trace-warnings --max-old-space-size=8192', 4 * gib)).toBe(
'--trace-warnings --max-old-space-size=8192 --max-old-space-size=3072'
)
})
})

View File

@ -50,6 +50,11 @@ describe('Electron runtime package contract', () => {
expect(scripts['build:release']).not.toContain('build:computer-macos')
})
it('runs the web build through the heap-sized Vite wrapper', () => {
expect(packageJson.scripts['build:web']).toContain('node config/scripts/run-vite-web-build.mjs')
expect(packageJson.scripts['build:web']).toContain('node config/scripts/verify-web-build.mjs')
})
it('guards release publishing before electron-builder runs', () => {
const releaseWorkflow = readFileSync(
join(projectDir, '.github/workflows/release-cut.yml'),
@ -63,7 +68,7 @@ describe('Electron runtime package contract', () => {
])
)
expect([...releaseCommands.keys()].sort()).toEqual(['linux', 'mac', 'win'])
expect([...releaseCommands.keys()].sort()).toEqual(['linux-arm64', 'linux-x64', 'mac', 'win'])
for (const command of releaseCommands.values()) {
expect(command).toContain('node config/scripts/ensure-native-runtime.mjs --runtime=electron')
expect(command).toContain('electron-builder')
@ -72,7 +77,10 @@ describe('Electron runtime package contract', () => {
)
}
expect(releaseCommands.get('mac')).toContain(' && ORCA_MAC_RELEASE=1 ')
expect(releaseCommands.get('linux')).toContain(' && pnpm exec electron-builder ')
expect(releaseCommands.get('linux-x64')).toContain(' && pnpm exec electron-builder ')
expect(releaseCommands.get('linux-x64')).toContain('--linux AppImage deb rpm --x64')
expect(releaseCommands.get('linux-arm64')).toContain('ORCA_LINUX_ARM64_RELEASE=1')
expect(releaseCommands.get('linux-arm64')).toContain('--linux AppImage deb rpm --arm64')
expect(releaseCommands.get('win')).toContain(
'; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; pnpm exec electron-builder '
)

View File

@ -1,19 +1,15 @@
import { spawn } from 'node:child_process'
import { createRequire } from 'node:module'
import path from 'node:path'
import { appendBuildOldSpaceOption } from './node-old-space-limit.mjs'
const require = createRequire(import.meta.url)
const electronVitePackageJson = require.resolve('electron-vite/package.json')
const electronViteCli = path.join(path.dirname(electronVitePackageJson), 'bin', 'electron-vite.js')
const requestedNodeOptions = '--max-old-space-size=4096'
const existingNodeOptions = process.env.NODE_OPTIONS?.trim()
// Release builds have started OOMing on GitHub's macOS runners during the
// renderer bundle, so we force a larger heap here in one shared entrypoint
// instead of relying on shell-specific env syntax in individual scripts.
const nodeOptions = existingNodeOptions
? `${existingNodeOptions} ${requestedNodeOptions}`
: requestedNodeOptions
// renderer bundle. Reserve memory on smaller hosts so the OS does not kill Vite.
const nodeOptions = appendBuildOldSpaceOption(process.env.NODE_OPTIONS)
const child = spawn(process.execPath, [electronViteCli, 'build', ...process.argv.slice(2)], {
stdio: 'inherit',

View File

@ -0,0 +1,33 @@
import { spawn } from 'node:child_process'
import { createRequire } from 'node:module'
import path from 'node:path'
import { appendBuildOldSpaceOption } from './node-old-space-limit.mjs'
const require = createRequire(import.meta.url)
const vitePackageJson = require.resolve('vite/package.json')
const viteCli = path.join(path.dirname(vitePackageJson), 'bin', 'vite.js')
// Why: Raspberry Pi and release runners can hit Node's default old-space
// ceiling, but smaller hosts still need memory left for the OS and bundler.
const nodeOptions = appendBuildOldSpaceOption(process.env.NODE_OPTIONS)
const child = spawn(
process.execPath,
[viteCli, 'build', '--config', 'vite.web.config.ts', ...process.argv.slice(2)],
{
stdio: 'inherit',
env: {
...process.env,
NODE_OPTIONS: nodeOptions
}
}
)
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal)
return
}
process.exit(code ?? 1)
})

View File

@ -8,11 +8,15 @@ export function getRequiredReleaseAssetNames(tag) {
const version = tag.replace(/^v/i, '')
return [
'latest-linux.yml',
'latest-linux-arm64.yml',
'latest-mac.yml',
'latest.yml',
'orca-linux.AppImage',
'orca-linux-arm64.AppImage',
`orca-ide_${version}_amd64.deb`,
`orca-ide_${version}_arm64.deb`,
`orca-ide-${version}.x86_64.rpm`,
`orca-ide-${version}.aarch64.rpm`,
'orca-windows-setup.exe',
'orca-windows-setup.exe.blockmap',
`Orca-${version}-mac.zip`,
@ -86,7 +90,12 @@ export async function verifyRequiredReleaseAssets({ repo, tag, token }) {
const assetsByName = new Map(release.assets.map((asset) => [asset.name, asset]))
const requiredNames = new Set(getRequiredReleaseAssetNames(tag))
const manifestNames = ['latest-linux.yml', 'latest-mac.yml', 'latest.yml']
const manifestNames = [
'latest-linux.yml',
'latest-linux-arm64.yml',
'latest-mac.yml',
'latest.yml'
]
for (const manifestName of manifestNames) {
const manifestAsset = assetsByName.get(manifestName)

View File

@ -45,12 +45,16 @@ describe('getRequiredReleaseAssetNames', () => {
)
})
it('includes the Linux RPM alongside the existing AppImage and deb names', () => {
it('includes x64 and arm64 Linux assets', () => {
expect(getRequiredReleaseAssetNames('v1.4.27')).toEqual(
expect.arrayContaining([
'latest-linux-arm64.yml',
'orca-linux.AppImage',
'orca-linux-arm64.AppImage',
'orca-ide_1.4.27_amd64.deb',
'orca-ide-1.4.27.x86_64.rpm'
'orca-ide_1.4.27_arm64.deb',
'orca-ide-1.4.27.x86_64.rpm',
'orca-ide-1.4.27.aarch64.rpm'
])
)
})
@ -100,4 +104,32 @@ describe('verifyRequiredReleaseAssets', () => {
).rejects.toThrow('Missing: Orca-1.4.27-arm64-mac.zip')
expect(latestMacAsset).toBeTruthy()
})
it('checks assets referenced by the Linux arm64 updater manifest', async () => {
const tag = 'v1.4.27'
const required = getRequiredReleaseAssetNames(tag)
const release = releaseWithAssets(tag, required)
const arm64Manifest = release.assets.find((asset) => asset.name === 'latest-linux-arm64.yml')
const fetchMock = vi
.fn()
.mockResolvedValueOnce(jsonResponse([release]))
.mockResolvedValueOnce(jsonResponse('version: 1.4.27\n'))
.mockResolvedValueOnce(
jsonResponse(
[
'version: 1.4.27',
'files:',
' - url: orca-linux-arm64.AppImage.blockmap',
'path: orca-linux-arm64.AppImage'
].join('\n')
)
)
.mockResolvedValue(jsonResponse('version: 1.4.27\n'))
vi.stubGlobal('fetch', fetchMock)
await expect(
verifyRequiredReleaseAssets({ repo: 'stablyai/orca', tag, token: 'token' })
).rejects.toThrow('Missing: orca-linux-arm64.AppImage.blockmap')
expect(arm64Manifest).toBeTruthy()
})
})

View File

@ -56,7 +56,7 @@
"audit:localization": "node config/scripts/audit-localization-coverage.mjs",
"build:cli": "tsc -p config/tsconfig.cli.json --outDir out --composite false --incremental false && node config/scripts/verify-cli-bin.mjs --fix-executable && node config/scripts/install-dev-cli.mjs",
"build:electron-vite": "node config/scripts/run-electron-vite-build.mjs",
"build:web": "vite build --config vite.web.config.ts && node config/scripts/verify-web-build.mjs",
"build:web": "node config/scripts/run-vite-web-build.mjs && node config/scripts/verify-web-build.mjs",
"build:desktop": "pnpm run typecheck && pnpm run build:relay && pnpm run build:cli && pnpm run build:electron-vite && pnpm run build:web",
"build": "pnpm run build:desktop && pnpm run build:native",
"build:release": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite && pnpm run build:web",
@ -68,7 +68,7 @@
"build:icons": "bash resources/icon-source/generate.sh",
"build:mac": "pnpm run build:desktop && pnpm run build:computer-macos && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --mac",
"build:mac:release": "node config/scripts/verify-macos-release-env.mjs && ORCA_MAC_RELEASE=1 pnpm run build:desktop && ORCA_MAC_RELEASE=1 pnpm run build:computer-macos && pnpm run ensure:electron-runtime && ORCA_MAC_RELEASE=1 electron-builder --config config/electron-builder.config.cjs --mac",
"build:linux": "pnpm run build:desktop && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --linux",
"build:linux": "pnpm run build:desktop && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --linux AppImage deb",
"test:e2e": "pnpm run ensure:electron-runtime && npx playwright test --config tests/playwright.config.ts --project electron-headless",
"test:e2e:terminal-rendering-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts --grep @terminal-rendering-golden --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:terminal-rendering-release-evidence": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts tests/e2e/terminal-long-table-scroll-restore.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=2",