From 7cdf0f70bcd414a395bbc4c81b2bde11eba28ded Mon Sep 17 00:00:00 2001 From: FaintGhost Date: Wed, 5 Aug 2026 01:28:33 +0900 Subject: [PATCH] feat(release): publish Linux ARM64 Broker Packs The broker-pack release matrix covers macOS arm64/x64, Windows x64, and Linux x64, but no Linux arm64. Users on arm64 hosts (Apple Silicon Docker, ARM VPSes) hit "Broker-pack catalog request failed: HTTP 404" when the installer requests OpenAlice-Broker-Packs--linux-arm64.json. GitHub-hosted runners are x64-only, so the arm64 matrix entry runs the install/build/upgrade-smoke sequence inside a linux/arm64 node:22-trixie container via QEMU; the workspace is bind-mounted so artifacts land on the runner for upload. pnpm deploys the platform-specific native SDKs (e.g. longbridge's glibc prebuild) for arm64 inside that container. broker-pack-upgrade-smoke now treats a missing previous-release catalog for the current platform as a first-release skip (new platform) instead of failing on the 404, while keeping every other HTTP error fatal. Verified locally: release-workflow/ci-workflow/desktop-package-workflow specs pass, tsc --noEmit clean, and a native linux-arm64 run of pnpm broker-packs:build produces and verifies all five engine archives. --- .github/workflows/release.yml | 34 ++++++++++++++++++++++++++++ docs/broker-packs.md | 7 +++--- scripts/broker-pack-upgrade-smoke.ts | 26 ++++++++++++++++++--- scripts/release-workflow.spec.ts | 1 + 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db3f29c0e..d76b25c89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -399,6 +399,12 @@ jobs: arch: x64 - os: ubuntu-latest arch: x64 + # GitHub-hosted runners are x64-only; Linux arm64 is emulated via + # QEMU inside a node:22-trixie container so pnpm deploys the + # platform-specific native SDKs (e.g. longbridge's glibc prebuild) + # for arm64. + - os: ubuntu-latest + arch: arm64 runs-on: ${{ matrix.os }} permissions: contents: read @@ -406,6 +412,10 @@ jobs: steps: - uses: actions/checkout@v7 + - name: Set up QEMU for arm64 emulation + uses: docker/setup-qemu-action@v3 + if: matrix.arch == 'arm64' + - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v7 @@ -413,13 +423,37 @@ jobs: node-version: 22 cache: pnpm + # arm64 has no native GitHub-hosted runner, so the whole install, build, + # and upgrade-smoke sequence runs inside a linux/arm64 container. The + # workspace is bind-mounted, so dist/broker-packs lands on the runner for + # the artifact step below. + - name: Build Broker Packs (arm64 emulated) + if: matrix.arch == 'arm64' + env: + PREVIOUS_TAG: ${{ needs.release.outputs.previous_tag }} + run: | + docker run --rm --platform linux/arm64 \ + --workdir /workspace \ + --volume "$PWD:/workspace" \ + --env COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + --env PREVIOUS_TAG="$PREVIOUS_TAG" \ + node:22-trixie bash -euo pipefail -c ' + corepack enable && corepack prepare pnpm@11.7.0 --activate + pnpm install --frozen-lockfile + pnpm broker-packs:build + pnpm broker-packs:upgrade-smoke -- --from "$PREVIOUS_TAG" + ' + - name: Install dependencies + if: matrix.arch != 'arm64' run: pnpm install --frozen-lockfile - name: Build optional Broker Packs + if: matrix.arch != 'arm64' run: pnpm broker-packs:build - name: Prove previous-release Broker Pack upgrade + if: matrix.arch != 'arm64' run: pnpm broker-packs:upgrade-smoke -- --from "${{ needs.release.outputs.previous_tag }}" - name: Preserve Broker Packs diff --git a/docs/broker-packs.md b/docs/broker-packs.md index ed42b7cbc..224c6f9d1 100644 --- a/docs/broker-packs.md +++ b/docs/broker-packs.md @@ -125,9 +125,10 @@ OpenAlice-Broker-Packs---.json OpenAlice-Broker----.tgz ``` -The release workflow runs this on macOS arm64, macOS x64, Windows x64, and -Linux x64; publishes the files with the desktop release; mirrors them to the -download CDN; and verifies every catalog and referenced archive. +The release workflow runs this on macOS arm64, macOS x64, Windows x64, Linux +x64, and Linux arm64 (emulated via QEMU on an x64 runner); publishes the files +with the desktop release; mirrors them to the download CDN; and verifies every +catalog and referenced archive. Before a candidate can publish, each platform runner downloads the real Broker Packs from the previous GitHub Release, activates them in an isolated diff --git a/scripts/broker-pack-upgrade-smoke.ts b/scripts/broker-pack-upgrade-smoke.ts index 50caa8bae..8b687bd11 100644 --- a/scripts/broker-pack-upgrade-smoke.ts +++ b/scripts/broker-pack-upgrade-smoke.ts @@ -56,9 +56,11 @@ async function main(): Promise { const candidateCatalog = await readCatalog( resolve(candidateRoot, brokerPackCatalogFileName(currentVersion)), ) - const previousCatalog = await fetchJson( - releaseAssetUrl(previousTag, brokerPackCatalogFileName(previousVersion)), - ) + // A platform that is new in this release (e.g. Linux arm64) has no + // previous-release catalog to upgrade from. Treat that as a first-release + // skip rather than a failure; any other HTTP error is still fatal. + const previousCatalog = await fetchPreviousCatalog(previousTag, previousVersion) + if (!previousCatalog) return assertCatalog(previousCatalog, previousVersion) assertCatalog(candidateCatalog, currentVersion) @@ -227,6 +229,24 @@ async function fetchJson(url: string): Promise { return JSON.parse((await fetchBytes(url)).toString('utf8')) as T } +async function fetchPreviousCatalog( + tag: string, + version: string, +): Promise { + const url = releaseAssetUrl(tag, brokerPackCatalogFileName(version)) + const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(120_000) }) + if (response.status === 404) { + console.log( + `[broker-pack-upgrade-smoke] previous release ${tag} has no ` + + `${process.platform}-${process.arch} catalog; platform is new in this release — ` + + 'skipping previous-release upgrade smoke', + ) + return null + } + if (!response.ok) throw new Error(`GET ${url} failed: HTTP ${response.status}`) + return JSON.parse(await response.text()) as BrokerPackReleaseCatalog +} + async function fetchBytes(url: string): Promise { const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(120_000) }) if (!response.ok) throw new Error(`GET ${url} failed: HTTP ${response.status}`) diff --git a/scripts/release-workflow.spec.ts b/scripts/release-workflow.spec.ts index 81c1faaed..5192470a4 100644 --- a/scripts/release-workflow.spec.ts +++ b/scripts/release-workflow.spec.ts @@ -46,6 +46,7 @@ describe('Release workflow critical path', () => { { os: 'macos-15-intel', arch: 'x64' }, { os: 'windows-latest', arch: 'x64' }, { os: 'ubuntu-latest', arch: 'x64' }, + { os: 'ubuntu-latest', arch: 'arm64' }, ]) expect(step(brokerPacks, 'Preserve Broker Packs').with?.['name']).toBe( 'broker-packs-${{ runner.os }}-${{ matrix.arch }}',