Skip to content

Packaged Bun runtime #167

Packaged Bun runtime

Packaged Bun runtime #167

name: Packaged Bun runtime
# Reusable, with TWO callers and no trigger of its own:
# - windows-proof-main.yml — post-merge on `main`, scoped by WINDOWS_SCOPE_PATHS
# - release.yml — unconditionally, in front of every job that publishes
# It briefly gated pull requests too; that cost +4m47 on every in-scope PR and was
# reversed. The 45-entry `on.pull_request.paths` filter that used to live here lives in
# WINDOWS_SCOPE_PATHS (src/shared/windows-ci-scope.ts), so one list serves both callers.
# See decisions/2026/08/06/windows-proof-post-merge-not-pull-request.md.
on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
jobs:
package-runtime:
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v5
- name: Setup build Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"
# Windows only, and the asymmetry is the point: the same command in the same run
# takes 4-5s on ubuntu, 10-36s on macOS and 255s on windows-latest — ~94k files
# materialised onto NTFS, not a slow download. Caching the posix legs would buy
# seconds while evicting entries from a repo cache already at its 10 GB ceiling.
# Same idiom and same shape as build.yml; the key must stay per-OS, caches do not
# travel between runners. See decisions/2026/08/06/required-checks-wait-for-windows-packaging.md.
- name: Restore node_modules cache (Windows only)
id: cache-deps
if: runner.os == 'Windows'
uses: actions/cache/restore@v5
with:
path: ./node_modules
key: bun-deps-windows-x64-${{ hashFiles('bun.lock') }}
- name: Install dependencies
# Skipped only on a Windows cache hit: on the posix legs the restore step above
# never ran, so cache-hit is empty and this stays unconditional there.
if: steps.cache-deps.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile
- name: Save node_modules cache (Windows only)
if: runner.os == 'Windows' && steps.cache-deps.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ./node_modules
key: bun-deps-windows-x64-${{ hashFiles('bun.lock') }}
- name: Native shell launch pure tests
run: bun run test:native-shell-launch
# Seq 1540: the Codex config dev3 writes into ~/.codex/config.toml. A raw
# Windows path in a TOML basic string makes `C:\Users` the escape `\U`, and
# codex then refuses to start machine-wide. The tests decode the generated
# file with a real TOML parser and drive ensureCodexConfigFile against a real
# temp home, so only the Windows leg can see the composition that broke; the
# POSIX legs are the control that the escaping did not change their output.
# The status hooks are part of that same file now — they used to ride in as a
# quoted `-c hooks={...}` argument and never survived the Windows command line
# (Seq 1540) — so their block is decoded here by the same real parser.
- name: Codex config generation + repair (real TOML parse)
run: bun run test:codex-config-windows
# Seq 1544: "+ Agent" on a native task handed the pane a hardcoded
# `/bin/bash`, so adding a second agent died on Windows with "requested
# shell executable not found". The spawn path is driven here against the
# runner's REAL platform; the POSIX legs are the control that the launch
# there is still `/bin/bash <script>.sh`.
# `always()`: main was red at an unrelated Codex step on 2026-08-14 (run
# 31839045471), and a step that only runs when an unrelated one passes is
# evidence about that step, not about this one.
- name: Agent spawn shell resolution (real platform)
if: always()
run: bun run test:agent-spawn-shell
# Seq 1737: the agent command line was POSIX-quoted and then re-parsed by the
# wrapper, so the apostrophe in "the task's title" reached PowerShell as `'\''`
# and every Claude launch on Windows died with a ParserError before the binary
# was looked up. Text is only half the question: PowerShell builds a raw command
# line and the callee's C runtime splits it again, and 5.1 escapes nothing on the
# way through. So the E2E RUNS the wrapper on this runner against a probe binary
# and compares its argv byte-for-byte, including the real system prompt and a
# binary whose path holds a space. The pure test pins both dialects and covers the
# call site, which an E2E calling the helper cannot.
# `always()`: a step that runs only when an unrelated one passes is evidence
# about that step, not about this one.
- name: Agent command line quoting (pure, both dialects)
if: always()
run: bunx vitest run --config vitest.config.bun.ts src/bun/__tests__/agent-launch-args.test.ts src/bun/__tests__/agent-command-golden.test.ts
- name: Agent command line executed against a real binary (${{ matrix.os }})
if: always()
timeout-minutes: 5
run: bun run test:agent-launch-args-e2e
# Seq 1547: the git-operation panes (rebase, push, merge) were hand-written
# `#!/bin/bash` launched through `/bin/bash`, so they were dead on Windows.
# These scripts rewrite history and touch the remote, so "the pane opened"
# proves nothing — the E2E RUNS them against throwaway repos on this runner
# and checks the operation completed: the remote ref moved, the rebase
# replayed, the squash commit landed with its subject intact. It also drives
# the conflict path, and it reads the verdict file back the way the app does
# (PowerShell 5.1's `>` writes UTF-16LE with a BOM, which would make every
# operation read as failed — invisible from macOS).
# The pure test pins the POSIX text and asserts the Windows properties.
# `always()` for the same reason as the step above: a step that runs only
# when an unrelated one passes is evidence about that step.
- name: Git-op pane scripts (pure, both dialects)
if: always()
run: bun run test:git-op-script
- name: Git-op panes executed against real repos (${{ matrix.os }})
if: always()
timeout-minutes: 6
run: bun run test:git-op-pane-e2e
# Seq 1546: the Dev Server button was dead on Windows — it refused with "the
# dev-server pane requires the tmux backend, which is POSIX-only" before it
# read anything (issue #1387, reproduced by Arseny on his own box), and the
# wrapper behind it was hand-written bash launched through `/bin/bash`.
# Swapping only the launch would have been WORSE than the outage: PowerShell
# would half-run bash text and look like it started. So the E2E RUNS the
# wrapper on this runner and checks the dev command executed and read back
# all three env blocks, and that a crashed dev server reports its code
# instead of hanging on its own keypress prompt with no keyboard attached.
# The pure test pins the POSIX text and asserts the Windows properties; the
# launch test covers the call site, which an E2E calling the helper cannot.
# `always()`: a step that runs only when an unrelated one passes is evidence
# about that step, not about this one.
- name: Dev-server pane script + launch (pure, both dialects)
if: always()
run: bun run test:dev-server-script
- name: Dev-server pane wrapper executed (${{ matrix.os }})
if: always()
timeout-minutes: 5
run: bun run test:dev-server-pane-e2e
- name: Windows self-update swap script (pure)
run: bun run test:windows-update
# The swap script is cmd.exe, tasklist and Get-Process — nothing about it can be
# proved from macOS. This runs it for real against a live process: once where the
# app exits on its own, once where it never does (the hang Arseny hit).
- name: Windows self-update swap, executed against a real process
if: matrix.os == 'windows-latest'
run: bun run test:windows-update-e2e
# THE PUBLISH PATH'S ONE WINDOWS-ONLY FAILURE MODE, RUN ON WINDOWS RATHER THAN
# ASSERTED ABOUT IT. `create-release-artifacts.sh` runs under Git Bash on the win-x64
# build, whose `pwd` speaks MSYS (`/d/a/...`) while bun resolves only `D:\a\...`; the
# first ever win-x64 job died on exactly that (run 31789301294). A macOS/Linux test
# cannot see this class of bug — there the dialects are the same string — and one
# already passed on the broken code. This job gates every publisher, so the proof
# lands BEFORE a build reaches the bucket. Runs on all three legs: the POSIX runs are
# the control that the suffix logic itself did not change.
- name: Release publish-version path (real shell, real bun)
run: bun run test:release-publish-version
# Seq 1383: what each OS actually shows for an argv0-named host. The
# per-platform assertions ARE the contract in decision 192, so they have to
# run on all three runners — negative ones included (macOS Activity Monitor
# and the Windows image-name column stay basename-only).
# From here on every independent proof carries `if: always()`, for the reason the
# pane-run step below already documents: these steps share nothing but the runner,
# so the first failure used to hide up to fourteen later verdicts and one flake read
# as a broad Windows outage. Build and upload steps deliberately keep the default —
# a failed proof must still publish nothing.
- name: Process naming visibility (${{ matrix.os }})
if: always()
run: bun run test:process-naming
# Real loopback proof for the Windows CLI control transport: round-trip,
# two instances, stale record, token rejection, and the loopback-only
# network boundary against this runner's real interfaces.
- name: CLI loopback transport E2E
if: always()
run: bun run test:cli-loopback-e2e
# The same transport driven through the COMPILED CLI (`dev3.exe` on this
# runner): representative commands, instance selection, stale/corrupt
# records, and the documented app-not-running exit code.
- name: Packaged CLI over loopback E2E
if: always()
timeout-minutes: 5
run: bun run test:cli-packaged-e2e
# Seq 1548: `dev3 pane run` EXECUTED rather than asserted about. The pane-run
# composition is authored per dialect and every existing test for it runs on
# macOS, where PowerShell does not exist — so this runs the real runner against a
# real spec file and reads the log back: non-ASCII output intact (the console code
# page would mangle it), a non-zero exit code intact, and — Windows only — both
# callers of the PowerShell lookup still launching with %SystemRoot% deleted from
# the environment, where they used to throw. `always()` so a neighbour's failure
# cannot hide this verdict.
- name: Pane run executed (real shell, real log)
if: always()
timeout-minutes: 5
run: bun run test:pane-run-exec
# Merged host-image manifest: generator, validator, deterministic assembly,
# additive staging outside the install root, and rollback selection.
- name: Packaged host image + manifest tests
if: always()
run: bun run test:native-host-image
# Seq 1550 investigation lever, dispatch-only because it costs minutes and gates
# nothing: the lifecycle E2E's journal-isolation check fails on windows-latest
# roughly one run in four, which no single run can settle. Two shells in ONE
# process with no registry, no journal and no fan-out — arm A shares the user
# profile as the E2E does, arm B gives each shell a private one — then the real
# fixture repeated, so a one-in-four flake shows up inside a single dispatch.
- name: Two-shell cross-session echo probe (dispatch only)
if: always() && github.event_name == 'workflow_dispatch'
env:
DEV3_ECHO_PROBE_ROUNDS: "12"
run: bun run test:native-cross-session-echo
- name: Repeat the lifecycle E2E (dispatch only)
if: always() && github.event_name == 'workflow_dispatch'
shell: bash
run: |
fails=0
for i in $(seq 1 8); do
echo "=== lifecycle repeat $i ==="
if ! bun run test:native-registry-e2e; then fails=$((fails + 1)); fi
done
echo "lifecycle repeats failed: $fails / 8"
# The fixture was changed to stop its own shell contaminating it, so the detector has
# to be shown still armed: put one session's bytes into another's journal on purpose
# and require the isolation check to name it, then restore both files.
#
# The mutation has to be a SHARED APPEND, not just a shared path: the writer replaces
# its whole file atomically, so two sessions pointed at one path merely overwrite each
# other and the last flush decides what a reader sees — a mutant that survived on
# windows-latest while dying on macOS, which says nothing about the check. Appending
# makes both sessions' frames present, which is the property under test. `perl -pi`
# because BSD and GNU `sed -i` disagree on the backup argument, `\s*` between the
# statements because a Windows checkout hands them over with CRLF endings, and the
# applied-or-not guard because a mutation that silently did not apply reads exactly
# like a mutant that survived.
- name: Mutation check (dispatch only) — GREEN means the mutant applied AND the E2E caught it
if: always() && github.event_name == 'workflow_dispatch'
shell: bash
run: |
set -e
echo "polarity: this step passes ONLY when the mutated E2E FAILS; red means the mutant survived or never applied"
paths=src/bun/native-terminal-registry/paths.ts
journal=src/bun/native-terminal-registry/journal.ts
restore() { git checkout -- "$paths" "$journal"; }
perl -pi -e 's{return join\(sessionDir\(id\), "journal\.ndjson"\);}{return join(sessionsRootDir(), "journal.ndjson");}' "$paths"
perl -pi -e 's{import \{ renameSync, writeFileSync \} from "node:fs";}{import { appendFileSync, renameSync, writeFileSync } from "node:fs";}' "$journal"
perl -0pi -e 's{const tmp = `\$\{this\.path\}\.\$\{process\.pid\}\.tmp`;\s*writeFileSync\(tmp, this\.frames\.join\(""\), \{ mode: 0o600 \}\);\s*renameSync\(tmp, this\.path\);}{appendFileSync(this.path, this.frames.join(""), { mode: 0o600 });}' "$journal"
git diff --stat "$paths" "$journal"
if ! git diff --quiet --exit-code -- "$paths" && ! git diff --quiet --exit-code -- "$journal"; then
echo "mutation applied to both files"
else
restore
echo "MUTATION NOT APPLIED — the patterns no longer match, so this check proves nothing"
exit 1
fi
if bun run test:native-registry-e2e; then
restore
echo "MUTATION SURVIVED — the isolation check no longer detects a foreign session's bytes"
exit 1
fi
restore
echo "mutation killed: the isolation check caught the foreign session's bytes"
# Real-runtime lifecycle regression for the persistent native-session
# registry — runs on native Windows + POSIX with the pinned Bun 1.3.14.
- name: Native-session registry lifecycle E2E
if: always()
run: bun run test:native-registry-e2e
# Seq 1236 force-kills only the recorded host while journal/parser writes
# are active, then proves owned-tree death and token-matched recovery.
- name: Native-session host crash recovery E2E
if: always()
run: bun run test:native-crash-e2e
# Seq 1237 writer/observer proof. The same real-runtime lifecycle runs on
# native Windows and POSIX: concurrent attach, claim races, reconnect, and resize.
- name: Native-session two-client lifecycle E2E
if: always()
run: bun run test:native-multi-client-e2e
# Seq 1247 app-restart proof. Two separate short-lived controller processes:
# controller A starts + marks + exits; controller B (a clean process)
# rediscovers and reattaches to the same host/shell/session/pane + state,
# with a deterministic single writer and honest lost-session results.
- name: Native-session app-restart reattach E2E
if: always()
run: bun run test:native-app-restart-e2e
# Seq 1381 cross-instance owner-routing proof for #1218. THREE separate
# processes against one host: A holds the writer lease and serves the CLI's
# NDJSON protocol, B binds the same pane as an observer and forwards one
# delivery to A, C reopens and counts what reached the PTY. Windows exercises
# the loopback `<pid>.endpoint.json` transport, POSIX the `<pid>.sock` one.
- name: Native-pane cross-instance owner-routing E2E
if: always()
timeout-minutes: 5
run: bun run test:native-owner-routing-e2e
# Seq 1371 + 1381 PRODUCT proof of the same routing, entered from outside:
# a `message.send` request arrives over the non-owning app's own CLI socket,
# resolves a real task off disk, routes to the owning app and lands in native
# pane-1 exactly once. POSIX only — the pane's shell is `stty -echo; exec cat`,
# which has no PowerShell equivalent, so the Windows leg of owner routing is
# covered by the E2E above instead.
- name: Native message owner-routing E2E
if: always() && runner.os != 'Windows'
timeout-minutes: 6
run: bun run test:native-message-e2e
# Seq 1292 PRODUCT proof: a task's primary terminal driven through
# startNativeTaskTerminal/attach/stop — create, shell round-trip, resize,
# detach, controller restart, observer refusal, owned-tree cleanup with a
# live tmux sentinel, an honest null after cleanup, and the renderer
# transport (pty-server WebSocket bridge with two real WS clients).
- name: Product native task-terminal E2E
if: always()
timeout-minutes: 5
run: bun run test:native-task-terminal-e2e
# Seq 1254 single-view adapter parity. Drives the shared backend-neutral
# parity corpus against the native adapter on native Windows + POSIX with
# the pinned Bun 1.3.14 (single-view live + pure scenarios; multi-view
# deferred to LAY-003/LAY-004). Expected final line: ALL CHECKS PASSED.
# Completes in ~20s; the step timeout turns a future non-exiting run into a
# fast, obvious failure instead of eating the job's whole 20-minute budget.
- name: Native single-view adapter parity E2E
if: always()
timeout-minutes: 5
run: bun run test:native-parity-e2e
# Seq 1283 native multi-pane coordinator. Runs the previously deferred
# multi-view scenarios with REAL panes (2 then 6, PowerShell on Windows):
# independent shells, non-crossing output, client-local focus/zoom,
# writer-vs-observer resize, fresh-process reconnect to the same pids,
# single-pane close, and full owned-tree cleanup.
- name: Native multi-pane coordinator E2E
if: always()
timeout-minutes: 6
run: bun run test:native-multipane-e2e
# Seq 1744: closing one pane must close that pane only. Driven through the
# close RPC the renderer calls, not through the pane helpers beside it, so a
# regression in the handler is caught too. The reported failure was on
# Windows and the code is platform-neutral, which is exactly why this leg
# runs it here as well as on POSIX.
- name: Pane close keeps the task's terminal E2E
if: always()
timeout-minutes: 6
run: bun run test:aux-pane-close-e2e
- name: Explicit Windows shell launch matrix
if: always() && runner.os == 'Windows'
shell: powershell
run: |
& .\src\bun\native-terminal-registry\__tests__\run-windows-shell-matrix.ps1 `
-OutDir "${{ runner.temp }}\dev3-windows-shell-matrix"
- name: Upload Windows shell launch evidence
if: always() && runner.os == 'Windows'
uses: actions/upload-artifact@v5
with:
name: windows-shell-launch-matrix
path: ${{ runner.temp }}\dev3-windows-shell-matrix\
# Seq 1228 live-parser proof. Gates ONLY the deferred path; `callback`
# mode is printed as evidence and is EXPECTED to fail on Windows
# Bun 1.3.14 (the preserved seq 1185 reproduction, decision 146).
- name: Live-parser regression probe (callback vs deferred)
if: always()
run: bun src/bun/native-terminal-registry/regression-probe.ts both
- name: Live-parser lifecycle E2E
if: always()
run: bun run test:native-live-parser-e2e
- name: Bundle terminal host entrypoint
if: runner.os == 'Windows'
run: bun run build:native
- name: Build packaged runtime tracer
working-directory: scripts/fixtures/windows-conpty-package
run: bun ../../../node_modules/electrobun/bin/electrobun.cjs build --env=canary
- name: Upload package and proof
if: runner.os == 'Windows'
uses: actions/upload-artifact@v5
with:
name: windows-conpty-package
path: |
scripts/fixtures/windows-conpty-package/artifacts/
# The REAL dev3 Windows package, not the isolated tracer fixture: proves the
# versioned native host image ships inside the final update archive and that
# it stages, launches detached, reattaches, and stops from there with no Bun
# on PATH. Separate job so its ~10 minutes do not eat the matrix job's budget.
windows-app-archive:
runs-on: windows-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@v5
- name: Setup build Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"
# Same 255s-on-Windows install as the matrix job above, paid a second time in
# parallel, so both jobs have to be cached or the gate's duration does not move.
# Same key on purpose: same lockfile, same runner, one entry serves both.
- name: Restore node_modules cache
id: cache-deps
uses: actions/cache/restore@v5
with:
path: ./node_modules
key: bun-deps-windows-x64-${{ hashFiles('bun.lock') }}
- name: Install dependencies
if: steps.cache-deps.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile
- name: Save node_modules cache
if: steps.cache-deps.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ./node_modules
key: bun-deps-windows-x64-${{ hashFiles('bun.lock') }}
# postBuild assembles + validates the host image inside the bundle;
# postPackage re-verifies it from the final `.tar.zst` and drives the
# detached lifecycle from the staged copy.
#
# `package:win-archive` passes electrobun `--env=canary`, and the build below is
# handed to a human as-is. That is safe under a CONDITION, not as a property: today
# the channel is a NAME only — electrobun's naming.ts suffixes artifact and app
# names and records the channel in build.json, nothing else, and no Windows
# update.json is published for any channel, so an unsigned CI build cannot behave
# differently from a stable one. Seq 1443 is landing real channel semantics; when
# it does, re-examine whether a human should get a canary-channel build here.
- name: Build the real dev3 Windows package and verify the final archive
run: bun run package:win-archive
# Whole-app proof on top of the native-host lifecycle one: the archive ships
# the desktop executable + bundled dev3.exe + manifest-validated host image,
# and the extracted executable reaches the ready marker and dies completely.
- name: Launch the packaged Windows app and prove clean shutdown
timeout-minutes: 10
env:
DEV3_REQUIRE_WINDOWS_PROOF: "1"
# Extract somewhere durable instead of a temp workspace that dies with the
# script, so the tree uploaded below is the one this proof actually spawned —
# not a look-alike re-extracted from the same archive afterwards.
DEV3_WINDOWS_APP_UNPACK_DIR: ${{ github.workspace }}\windows-app-unpacked
run: bun run verify:win-app-launch
- name: Upload real Windows archive proof
if: always()
uses: actions/upload-artifact@v5
with:
name: windows-app-archive-proof
path: |
artifacts/windows-conpty-package-proof.json
artifacts/windows-app-layout.json
artifacts/windows-app-launch-proof.json
artifacts/*-update.json
# The downloadable build, deliberately NOT folded into the proof artifact above:
# that one is JSON for CI and gets opened constantly, this one is the whole app for a
# human and gets opened rarely. Merging them would make every proof download drag the
# payload behind it.
#
# SIZES, MEASURED, because ~400 MB was quoted here for a long time and is only true of
# the tree on disk: GitHub compresses an artifact, so this one downloads at ~124 MB
# (windows-app-e4b5fcbf5…, 130 055 428 bytes) and the release zip built from the same
# tree is 121.0 MB (run 31799430704, on windows-latest).
#
# The EXTRACTED tree, not the `.tar.zst`: Windows tar.exe cannot read zstd (this
# job ships electrobun's zig-zstd.exe precisely because of that), so the archive is
# unopenable without extra tooling, while GitHub zips an artifact on download and
# Explorer extracts it with nothing installed. The self-extracting Setup .exe the
# build also produces is NOT uploaded: nothing has ever launched it, and the file a
# summary tells a human to open must be a file the proof launched. That installer
# is a KNOWN GAP, not a rejected idea — see
# decisions/2026/08/06/downloadable-windows-build-is-the-launched-tree.md before assuming
# it was vetted.
#
# NO `if: always()` here, on purpose: a failed launch proof must publish nothing.
# `always()` would look like a debugging convenience and would instead hand out
# builds that never reached a window, wearing a green download banner.
- name: Upload the launched Windows app for download
uses: actions/upload-artifact@v5
with:
name: windows-app-${{ github.sha }}
path: windows-app-unpacked/
# 30 days, not the 90-day default. Storage is free on a public repo, so this is
# not a cost call: a months-old build still listed is one somebody downloads
# believing it is current. An expired build is re-run, not lost.
retention-days: 30
# An empty upload would publish a green "download it here" summary pointing at
# nothing at all.
if-no-files-found: error
# Reads the launch proof and renders the entry point out of it, so this text cannot
# name an executable the proof did not start. Same reason it has no `if:` as above.
- name: Explain how to run the downloadable Windows build
env:
DEV3_WINDOWS_ARTIFACT_NAME: windows-app-${{ github.sha }}
DEV3_WINDOWS_ARTIFACT_RETENTION_DAYS: "30"
run: bun scripts/windows-download-summary.ts
# The REAL dev3 macOS and Linux packages. Synthesized bundle trees in vitest
# cannot prove a package ships a launchable host, so this builds the actual
# package, lets postBuild assemble + stage + drive the detached host with no
# Bun on PATH, and then proves a bundled resolver (no source checkout, no env
# override) reaches that image from the packaged runtime.
posix-app-package:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- name: Setup build Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"
- name: Install dependencies
run: bun install --frozen-lockfile
# postBuild → scripts/package-posix-native-host.ts: assemble the versioned
# image into the bundle, stage it outside the install root, then version /
# start / reattach / stop the detached host from the staged copy.
- name: Build the real dev3 package
run: bun run build
- name: Resolve the packaged host image from the packaged runtime
run: bun scripts/verify-packaged-host-resolution.ts
- name: Upload package proof
if: always()
uses: actions/upload-artifact@v5
with:
name: posix-native-host-package-proof-${{ matrix.os }}
path: |
build/*/native-host-package-proof.json
build/*/native-host-resolution-proof.json