chore(ci): speed up the beta desktop publish (#55919)

This commit is contained in:
Luke Parker
2026-10-16 13:46:53 +10:00
committed by GitHub
Unverified
parent deb5cba571
commit 8ba78414d2
9 changed files with 324 additions and 32 deletions
+3
View File
@@ -58,6 +58,9 @@ runs:
with:
path: ${{ steps.cache.outputs.dir }}
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ steps.cache.outputs.version }}-${{ hashFiles('bun.lock', 'patches/**') }}
# Each dev merge changes beta's bun.lock. Beta falls back to the newest cache for the same Bun version
# (Bun's cache stores every package version separately) instead of installing everything from npm.
restore-keys: ${{ (github.ref_name == 'beta' && format('{0}-{1}-bun-{2}-', runner.os, runner.arch, steps.cache.outputs.version)) || '' }}
- name: Install setuptools for distutils compatibility
run: python3 -m pip install setuptools || pip install setuptools || true
+173 -13
View File
@@ -28,12 +28,18 @@ on:
required: false
type: string
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ (github.ref_name == 'dev' && (inputs.version || inputs.bump) && 'release') || inputs.version || inputs.bump }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ (github.ref_name == 'dev' && (inputs.version || inputs.bump) && 'release') || inputs.version || inputs.bump }}
# Beta ships only the newest commit to the team. A cancelled beta run can leave unreferenced files in R2, but only
# its publish job records a release, in one request.
cancel-in-progress: ${{ github.ref_name == 'beta' }}
permissions:
id-token: write
contents: write
packages: write
# Beta desktop jobs poll this run's artifacts while the CLI builds.
actions: read
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'dev' && !inputs.bump && !inputs.version && 'dev') || '' }}
@@ -42,15 +48,35 @@ jobs:
version:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode'
env:
BETA: ${{ github.ref_name == 'beta' }}
steps:
# A beta version only depends on the run number, so beta computes it without a checkout or an install.
- name: Beta version
id: beta
if: env.BETA == 'true'
run: |
# Matches packages/script: 0.0.0-<channel>-<run number>[.<attempt>] for preview channels.
version="0.0.0-beta-${GITHUB_RUN_NUMBER}"
if [ "$GITHUB_RUN_ATTEMPT" != "1" ]; then version="${version}.${GITHUB_RUN_ATTEMPT}"; fi
{
echo "version=$version"
echo "release=true"
echo "tag=v$version"
echo "repo=anomalyco/opencode-beta"
} >> "$GITHUB_OUTPUT"
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
if: env.BETA != 'true'
with:
fetch-depth: 0
- uses: ./.github/actions/setup-bun
if: env.BETA != 'true'
- name: Setup git committer
id: committer
if: env.BETA != 'true'
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
@@ -61,6 +87,7 @@ jobs:
run: bun i -g opencode-ai
- id: version
if: env.BETA != 'true'
run: |
./script/version.ts
env:
@@ -70,16 +97,17 @@ jobs:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GH_REPO: ${{ (github.ref_name == 'beta' && 'anomalyco/opencode-beta') || github.repository }}
outputs:
version: ${{ steps.version.outputs.version }}
release: ${{ steps.version.outputs.release }}
tag: ${{ steps.version.outputs.tag }}
repo: ${{ steps.version.outputs.repo }}
version: ${{ steps.version.outputs.version || steps.beta.outputs.version }}
release: ${{ steps.version.outputs.release || steps.beta.outputs.release }}
tag: ${{ steps.version.outputs.tag || steps.beta.outputs.tag }}
repo: ${{ steps.version.outputs.repo || steps.beta.outputs.repo }}
build-cli:
needs: version
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 30
if: github.repository == 'anomalyco/opencode'
# Beta builds only the CLIs its desktop app bundles, in build-cli-beta.
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
@@ -138,6 +166,48 @@ jobs:
outputs:
version: ${{ needs.version.outputs.version }}
# Beta publishes only the desktop app, so it builds just the 6 CLIs the desktop bundles, each in its own job and
# artifact. Desktop jobs start alongside and wait for their CLI only when they need it.
build-cli-beta:
needs: version
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 30
if: github.repository == 'anomalyco/opencode' && github.ref_name == 'beta'
strategy:
fail-fast: false
matrix:
# macOS first: notarization makes those desktop jobs the longest.
target:
- darwin-x64-baseline
- darwin-arm64
- linux-arm64
- windows-arm64
- windows-x64-baseline
- linux-x64-baseline
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/setup-bun
with:
bun-version: 1.4.2
- name: Build CLI
run: ./packages/cli/script/build.ts --target=opencode-${{ matrix.target }}
env:
BUN_COMPILE_RELEASE: bun-v1.4.2
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
GH_REPO: ${{ needs.version.outputs.repo }}
GH_TOKEN: ${{ github.token }}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-cli-unsigned-cli-${{ matrix.target }}
path: packages/cli/dist/cli-${{ matrix.target }}
# Deflate level 1 compresses a CLI binary twice as fast as the default for 7% more bytes.
compression-level: 1
if-no-files-found: error
sign-cli-macos:
needs: build-cli
runs-on: macos-26
@@ -344,10 +414,11 @@ jobs:
build-electron:
# Desktop packaging re-signs the bundled CLI on macOS and Windows, so it starts from the unsigned build.
# Beta skips build-cli and waits for its own CLI from build-cli-beta just before Prepare.
needs:
- version
- build-cli
if: github.repository == 'anomalyco/opencode' && ((github.ref_name == 'dev' && needs.version.outputs.release != '') || github.ref_name == 'beta')
if: ${{ !cancelled() && !failure() && github.repository == 'anomalyco/opencode' && ((github.ref_name == 'dev' && needs.version.outputs.release != '') || github.ref_name == 'beta') }}
continue-on-error: false
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
@@ -359,34 +430,44 @@ jobs:
strategy:
fail-fast: false
matrix:
# Beta packages macOS x64 on Apple Silicon and Windows arm64 on Blacksmith. electron-builder's afterPack rejects
# a bundle whose CLI or node-pty does not match the target CPU.
settings:
- host: macos-26-intel
- host: ${{ (github.ref_name == 'beta' && 'macos-26') || 'macos-26-intel' }}
target: x86_64-apple-darwin
cli: darwin-x64-baseline
platform_flag: --mac --x64
bun_install_flags: --os=darwin --cpu=x64
bun_install_flags: ${{ (github.ref_name == 'beta' && '--os=darwin --cpu=*') || '--os=darwin --cpu=x64' }}
- host: macos-26
target: aarch64-apple-darwin
cli: darwin-arm64
platform_flag: --mac --arm64
bun_install_flags: --os=darwin --cpu=arm64
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
- host: "windows-2025"
# Releases keep github-hosted Windows from the Tauri build, which needed an ARM64 MSVC toolchain; Electron
# packaging compiles nothing.
- host: ${{ (github.ref_name == 'beta' && 'blacksmith-4vcpu-windows-2025') || 'windows-2025' }}
target: aarch64-pc-windows-msvc
cli: windows-arm64
platform_flag: --win --arm64
bun_install_flags: --cpu=*
- host: "blacksmith-4vcpu-windows-2025"
target: x86_64-pc-windows-msvc
cli: windows-x64-baseline
platform_flag: --win
- host: "blacksmith-4vcpu-ubuntu-2404"
target: x86_64-unknown-linux-gnu
cli: linux-x64-baseline
platform_flag: --linux
- host: "blacksmith-4vcpu-ubuntu-2404-arm"
target: aarch64-unknown-linux-gnu
cli: linux-arm64
platform_flag: --linux --arm64
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name != 'beta'
with:
name: opencode-preview-cli-unsigned
path: packages/cli/dist
@@ -442,6 +523,48 @@ jobs:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
# Beta trades larger installers for faster packaging. These variables must stay unset elsewhere:
# electron-builder rejects an empty ELECTRON_BUILDER_COMPRESSION_LEVEL.
- name: Beta packaging settings
if: github.ref_name == 'beta'
shell: bash
run: |
# fpm sets XZ_OPT for the deb and rpm level, but xz still reads XZ_DEFAULTS, so it uses every CPU.
if [ "$RUNNER_OS" = "Linux" ]; then echo "XZ_DEFAULTS=-T0" >> "$GITHUB_ENV"; fi
# The macOS zip (7z deflate, default -mx=7) and DMG (UDZO zlib, default level 9) at level 1.
if [ "$RUNNER_OS" = "macOS" ]; then echo "ELECTRON_BUILDER_COMPRESSION_LEVEL=1" >> "$GITHUB_ENV"; fi
# Beta builds the app before its CLI exists. `bun run build` would run the prebuild hook, which copies the CLI;
# Prepare runs it once the CLI is here.
- name: Build (beta)
if: github.ref_name == 'beta'
run: bun run electron-vite build
working-directory: packages/desktop
env:
NODE_OPTIONS: --max-old-space-size=4096
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_CHANNEL: beta
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
VITE_SENTRY_ENVIRONMENT: beta
VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
- name: Wait for the CLI (beta)
if: github.ref_name == 'beta'
run: bun ./script/wait-for-artifact.ts opencode-cli-unsigned-cli-${{ matrix.settings.cli }} "build-cli-beta (${{ matrix.settings.cli }})"
env:
GITHUB_TOKEN: ${{ github.token }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name == 'beta'
with:
name: opencode-cli-unsigned-cli-${{ matrix.settings.cli }}
path: packages/cli/dist/cli-${{ matrix.settings.cli }}
- name: Prepare
run: bun ./scripts/prepare.ts
working-directory: packages/desktop
@@ -452,6 +575,7 @@ jobs:
OPENCODE_CLI_DIST: ${{ github.workspace }}/packages/cli/dist
- name: Build
if: github.ref_name != 'beta'
run: bun run build
working-directory: packages/desktop
env:
@@ -492,7 +616,8 @@ jobs:
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
- name: Create macOS .app.tar.gz
if: runner.os == 'macOS' && needs.version.outputs.release
# Only the retired Tauri updater feed read these archives.
if: runner.os == 'macOS' && needs.version.outputs.release && github.ref_name != 'beta'
working-directory: packages/desktop/dist
run: |
if [[ "${{ matrix.settings.target }}" == "x86_64-apple-darwin" ]]; then
@@ -529,10 +654,29 @@ jobs:
}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: github.ref_name != 'beta'
with:
name: opencode-desktop-${{ matrix.settings.target }}
path: packages/desktop/dist/*
# Beta uploads its installers to R2 here instead of passing them through an artifact to publish, which only
# records the release from the returned file list.
- name: Upload to R2 (beta)
if: github.ref_name == 'beta'
run: bun ./scripts/upload.ts dist dist/uploaded-${{ matrix.settings.target }}.json
working-directory: packages/desktop
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
CLOUDFLARE_ACCOUNT_ID: 15d29c8639fd3733b1b5486a2acfd968
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: github.ref_name == 'beta'
with:
name: desktop-uploaded-${{ matrix.settings.target }}
path: packages/desktop/dist/uploaded-${{ matrix.settings.target }}.json
if-no-files-found: error
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: needs.version.outputs.release
with:
@@ -543,6 +687,7 @@ jobs:
needs:
- version
- build-cli
- build-cli-beta
- sign-cli-macos
- build-node-app-archive
- build-node-cli
@@ -555,7 +700,9 @@ jobs:
- uses: ./.github/actions/setup-bun
# Beta only records the desktop release, so it skips the Docker, git and AUR setup.
- name: Login to GitHub Container Registry
if: github.ref_name != 'beta'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
@@ -563,9 +710,11 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
if: github.ref_name != 'beta'
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
if: github.ref_name != 'beta'
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
@@ -611,20 +760,29 @@ jobs:
path: /tmp/latest-yml
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: needs.version.outputs.release
if: needs.version.outputs.release && github.ref_name != 'beta'
with:
pattern: opencode-desktop-*
path: /tmp/desktop
merge-multiple: true
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name == 'beta'
with:
pattern: desktop-uploaded-*
path: /tmp/desktop-uploaded
merge-multiple: true
- name: Setup git committer
id: committer
if: github.ref_name != 'beta'
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Cache apt packages (AUR)
if: github.ref_name != 'beta'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: /var/cache/apt/archives
@@ -633,6 +791,7 @@ jobs:
${{ runner.os }}-apt-aur-
- name: Setup SSH for AUR
if: github.ref_name != 'beta'
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
@@ -656,6 +815,7 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
OPENCODE_DESKTOP_DIST: /tmp/desktop
OPENCODE_DESKTOP_UPLOADED: ${{ (github.ref_name == 'beta' && '/tmp/desktop-uploaded') || '' }}
WINGET_GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: 15d29c8639fd3733b1b5486a2acfd968
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+8 -4
View File
@@ -20,16 +20,20 @@ export async function buildAppArchive(channel: string, options?: { skipBuild?: b
.map(async (key) => {
const body = Buffer.from(await Bun.file(path.join(root, "dist", key)).arrayBuffer())
// Independent entries let the server materialize only assets the browser requests.
return [key, await compress(body)] as const
return [key, await compress(body, channel)] as const
}),
),
)
}
// The assets compress once per release, so they take brotli's best quality: about 11% smaller than quality 6 for the
// web UI. Each runs on the thread pool, which keeps the build to about a minute.
function compress(body: Buffer) {
// web UI. Each runs on the thread pool, which keeps the build to about a minute. Beta builds on every push and only
// bundles the CLI into the desktop app, so it keeps quality 6, which takes a few seconds.
function compress(body: Buffer, channel: string) {
return promisify(brotliCompress)(body, {
params: { [constants.BROTLI_PARAM_QUALITY]: 11, [constants.BROTLI_PARAM_SIZE_HINT]: body.length },
params: {
[constants.BROTLI_PARAM_QUALITY]: channel === "beta" ? 6 : 11,
[constants.BROTLI_PARAM_SIZE_HINT]: body.length,
},
})
}
+5 -1
View File
@@ -266,7 +266,11 @@ async function compileExecutable(item: (typeof allTargets)[number]) {
function compileReleaseAssets(release: string) {
const existing = releaseAssets.get(release)
if (existing) return existing
const pending = fetch(`https://api.github.com/repos/oven-sh/bun/releases/tags/${release}?cache=${Date.now()}`)
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN
// Beta builds each CLI in its own job; authenticated requests avoid the per-IP limit on shared runners.
const pending = fetch(`https://api.github.com/repos/oven-sh/bun/releases/tags/${release}?cache=${Date.now()}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
.then(async (response) => {
if (!response.ok) throw new Error(`Failed to resolve Bun release ${release}: ${response.status}`)
const data: unknown = await response.json()
+6 -2
View File
@@ -27,6 +27,10 @@ const legacyDesktopEntryFpm = `${legacyDesktopEntry}=/usr/share/applications/ope
const metainfoFpm = (appId: string) =>
`${path.join(packageDir, "resources", `${appId}.metainfo.xml`)}=/usr/share/metainfo/${appId}.metainfo.xml`
// Beta packages every commit for the team, so it trades about 5% (deb) and 17% (rpm) larger files for xz -1, which
// compresses the payload about 3x faster than the deb default (-3) and 5x faster than the rpm default (-9).
const betaCompressionFpm = (target: "deb" | "rpm") => [`--${target}-compression-level`, "1"]
async function signWindows(configuration: { path: string }) {
if (process.platform !== "win32") return
@@ -238,8 +242,8 @@ export function getConfig() {
url: "https://opencode.ai/update/api/beta/desktop/opencode/",
channel: "latest",
},
deb: { fpm: [metainfoFpm(appId)] },
rpm: { packageName: "opencode-beta", fpm: [metainfoFpm(appId)] },
deb: { fpm: [...betaCompressionFpm("deb"), metainfoFpm(appId)] },
rpm: { packageName: "opencode-beta", fpm: [...betaCompressionFpm("rpm"), metainfoFpm(appId)] },
}
}
+35 -11
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bun
import { Script } from "@opencode/script"
import { Schema } from "effect"
import { UpdateArtifact } from "../../../script/update-artifact"
const dryRun = process.argv.includes("--dry-run")
@@ -10,19 +11,14 @@ if (!Script.release) {
process.exit(0)
}
const directory = process.env.OPENCODE_DESKTOP_DIST
const UploadedFiles = Schema.fromJsonString(
Schema.Record(Schema.String, Schema.Struct({ url: Schema.String, sha256: Schema.String, size: Schema.Number })),
)
if (!directory) throw new Error("OPENCODE_DESKTOP_DIST is required")
// Beta build jobs upload their own platform's files (scripts/upload.ts) and pass only the returned file lists here.
const uploadedDirectory = process.env.OPENCODE_DESKTOP_UPLOADED
const files = (
await Array.fromAsync(
new Bun.Glob("*.{exe,blockmap,dmg,zip,AppImage,deb,rpm,app.tar.gz}").scan({ cwd: directory, absolute: true }),
)
).sort()
if (!files.length) throw new Error("No desktop release files found")
const uploaded = await UpdateArtifact.upload({ version: Script.version, files, dryRun })
const uploaded = uploadedDirectory ? await uploadedByBuildJobs(uploadedDirectory) : await uploadFiles()
const artifact = {
channel: Script.channel,
@@ -36,6 +32,34 @@ if (dryRun) console.log(`dry-run artifact: ${JSON.stringify(artifact)}`)
if (!dryRun) await UpdateArtifact.publish(artifact)
async function uploadFiles() {
const directory = process.env.OPENCODE_DESKTOP_DIST
if (!directory) throw new Error("OPENCODE_DESKTOP_DIST is required")
const files = (
await Array.fromAsync(
new Bun.Glob("*.{exe,blockmap,dmg,zip,AppImage,deb,rpm,app.tar.gz}").scan({ cwd: directory, absolute: true }),
)
).sort()
if (!files.length) throw new Error("No desktop release files found")
return UpdateArtifact.upload({ version: Script.version, files, dryRun })
}
async function uploadedByBuildJobs(directory: string) {
const lists = await Array.fromAsync(new Bun.Glob("*.json").scan({ cwd: directory, absolute: true }))
if (!lists.length) throw new Error(`No uploaded desktop file lists found in ${directory}`)
const entries = await Promise.all(
lists.map(async (file) => Object.entries(Schema.decodeUnknownSync(UploadedFiles)(await Bun.file(file).text()))),
)
return Object.fromEntries(entries.flat())
}
type DesktopFile = {
url: string
sha512: string
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bun
// Uploads one platform's desktop release files to R2 from its build job and writes the returned file list, which
// publish.ts reads through OPENCODE_DESKTOP_UPLOADED instead of uploading every platform itself.
// Usage: bun scripts/upload.ts <dist directory> <output json>
import path from "node:path"
import { UpdateArtifact } from "../../../script/update-artifact"
const [directory, output] = process.argv.slice(2)
if (!directory || !output) throw new Error("Usage: bun scripts/upload.ts <dist directory> <output json>")
const version = process.env.OPENCODE_VERSION
if (!version) throw new Error("OPENCODE_VERSION is required")
const files = (
await Array.fromAsync(
new Bun.Glob("*.{exe,blockmap,dmg,zip,AppImage,deb,rpm,app.tar.gz}").scan({
cwd: path.resolve(directory),
absolute: true,
}),
)
).sort()
if (!files.length) throw new Error(`No desktop release files found in ${directory}`)
await Bun.write(output, JSON.stringify(await UpdateArtifact.upload({ version, files }), null, 2))
+2 -1
View File
@@ -131,7 +131,8 @@ async function prepareCli(dest: string) {
if (process.platform !== "win32") await chmod(dest, 0o755)
if (process.platform === "win32" && process.env.GITHUB_ACTIONS === "true") {
// electron-builder signs the bundled CLI again in Package, so beta signs it only there.
if (process.platform === "win32" && process.env.GITHUB_ACTIONS === "true" && resolveChannel() !== "beta") {
await $`pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File ../../script/sign-windows.ps1 ${dest}`
}
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bun
// Waits until another job of this workflow run uploads an artifact, so a job can do its other work while that job
// runs. Fails as soon as the producing job ends without the artifact.
// Usage: bun script/wait-for-artifact.ts <artifact name> <producing job name>
const [artifact, producer] = process.argv.slice(2)
if (!artifact || !producer) throw new Error("Usage: bun script/wait-for-artifact.ts <artifact> <job>")
const token = process.env.GITHUB_TOKEN
if (!token) throw new Error("GITHUB_TOKEN is required")
const run = `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
const started = Date.now()
while (Date.now() - started < 30 * 60_000) {
if (await uploaded()) {
console.log(`${artifact} is ready after ${Math.round((Date.now() - started) / 1000)}s`)
process.exit(0)
}
const conclusion = await producerConclusion()
if (conclusion && conclusion !== "success") throw new Error(`${producer} finished with ${conclusion}`)
// A job can finish just before its artifact appears in the listing.
if (conclusion === "success" && (await uploaded())) process.exit(0)
if (conclusion === "success") throw new Error(`${producer} succeeded without uploading ${artifact}`)
await Bun.sleep(5_000)
}
throw new Error(`Timed out waiting for ${artifact}`)
async function uploaded() {
const body = await get(`${run}/artifacts?name=${encodeURIComponent(artifact)}`)
return isRecord(body) && typeof body.total_count === "number" && body.total_count > 0
}
async function producerConclusion() {
const body = await get(`${run}/attempts/${process.env.GITHUB_RUN_ATTEMPT ?? "1"}/jobs?per_page=100`)
const jobs = isRecord(body) && Array.isArray(body.jobs) ? body.jobs : []
const job = jobs.find((item) => isRecord(item) && item.name === producer)
if (!isRecord(job) || job.status !== "completed") return undefined
return typeof job.conclusion === "string" ? job.conclusion : "unknown"
}
async function get(url: string): Promise<unknown> {
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" },
})
if (!response.ok) throw new Error(`GitHub API ${response.status} for ${url}`)
return response.json()
}
function isRecord(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input !== null && !Array.isArray(input)
}