Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/publish-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile --ignore-scripts

- name: Apply native engine patches
run: node packages/omo-native/bin/senpi-patch.mjs

- name: Validate release inputs
id: validate
env:
Expand Down Expand Up @@ -381,6 +384,31 @@ jobs:
esac
timeout-minutes: 20

- name: Smoke compiled workers and RPC
if: steps.release-assets.outputs.binary_exists != 'true'
env:
SMOKE_DIR: ${{ runner.temp }}/release-binary-rpc
run: |
set -euo pipefail
TARGET="${{ matrix.platform }}"
case "$TARGET" in
darwin-arm64|linux-x64|linux-x64-baseline|windows-x64|windows-x64-baseline)
trap 'rm -rf "$SMOKE_DIR"' EXIT
BINARY=".omo/release-binaries/omo-${TARGET}"
if [[ "$TARGET" == windows-* ]]; then BINARY="${BINARY}.exe"; fi
bun test script/senpi-worker-compile.test.ts
bun script/qa/dependency-audit-capture.ts --phase post \
--binary "$BINARY" --out "$SMOKE_DIR" --case rpc --case extension
jq -e '.complete == true and .pass == true and ([.cases[].case] == ["rpc", "extension"]) and all(.cases[]; .pass == true and .exitCode == 0)' "$SMOKE_DIR/summary.json"
;;
*)
# Non-native legs retain their existing platform smoke policy.
# Windows arm64 remains checksum-only: runtime behavior is unverified.
echo "Compiled RPC smoke requires a native runner for ${TARGET}"
;;
esac
timeout-minutes: 10

- name: Upload release binary artifact
if: steps.release-assets.outputs.binary_exists != 'true'
uses: actions/upload-artifact@v7
Expand Down
105 changes: 105 additions & 0 deletions .github/workflows/release-binary-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Release binary smoke

on:
pull_request:
paths:
- script/build-omo-binary.ts
- script/build-omo-binary.test.ts
- script/senpi-worker-compile*.ts
- packages/omo-native/**
- script/qa/dependency-audit-*.ts
- script/qa/dependency-audit/**
- script/qa/fixtures/dependency-audit/**
- script/release-compile-argv.fixture.ts
- script/receipt-gate.fixture.ts
- script/release-binary-smoke-workflow.test.ts
- script/publish-release-platform-workflow.test.ts
- .github/workflows/publish-platform.yml
- package.json
- bun.lock
- .github/workflows/release-binary-smoke.yml

permissions:
contents: read

jobs:
smoke:
runs-on: ${{ matrix.os }}
timeout-minutes: 45
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: linux-x64
binary: omo-linux-x64
- os: macos-latest
target: darwin-arm64
binary: omo-darwin-arm64
- os: windows-latest
target: windows-x64
binary: omo-windows-x64.exe
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "24"
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.4.2"
- name: Install dependencies
id: install
run: bun install --frozen-lockfile --ignore-scripts
- name: Apply native engine patches
id: patch
run: node packages/omo-native/bin/senpi-patch.mjs
- name: Build release binary
id: build
env:
OUT_DIR: ${{ runner.temp }}/release-binary-smoke
run: |
bun run script/build-omo-binary.ts \
--target "${{ matrix.target }}" \
--omo-version 0.0.0-ci \
--omo-ai-version 0.0.0-ci \
--out-dir "$OUT_DIR"
- name: Test compile and worker contracts
id: contracts
run: |
# The sidecar parity test reads the source plugin's generated skills.
node packages/omo-senpi/plugin/scripts/sync-skills.mjs
bun test script/build-omo-binary.test.ts script/senpi-worker-compile.test.ts
bun test script/release-binary-smoke-workflow.test.ts script/publish-release-platform-workflow.test.ts script/qa/dependency-audit-capture.test.ts
- name: Capture compiled RPC and extension receipts
id: capture
env:
OUT_DIR: ${{ runner.temp }}/release-binary-smoke
run: |
bun script/qa/dependency-audit-capture.ts --phase post \
--binary "$OUT_DIR/${{ matrix.binary }}" --out "$OUT_DIR/post" \
--case bytes --case graph --case rpc --case extension
jq -e '.complete == true and .pass == true and ([.cases[].case] == ["bytes", "graph", "rpc", "extension"]) and all(.cases[]; .pass == true and .exitCode == 0)' "$OUT_DIR/post/summary.json"
- name: Upload JSON receipts only
id: receipts
if: always()
uses: actions/upload-artifact@v7
with:
name: release-binary-smoke-${{ matrix.target }}
path: ${{ runner.temp }}/release-binary-smoke/post/*.json
if-no-files-found: error
retention-days: 7
- name: Write job summary
id: summary
if: always()
env:
JOB_SUMMARY_TITLE: Release binary smoke (${{ matrix.target }})
JOB_SUMMARY_STATUS: ${{ job.status }}
JOB_SUMMARY_DETAILS: |
- Builds the native release target with splitting and name-preserving minification.
- Tests relocated workers and compiled RPC/extension behavior.
- Uploads JSON receipts only; never publishes a binary.
JOB_SUMMARY_NEXT: Inspect the first failed build, contract, or receipt gate for this target.
run: GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" bash .github/scripts/write-job-summary.sh
65 changes: 65 additions & 0 deletions script/build-omo-binary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import {
mkdtempSync,
readFileSync,
readdirSync,
realpathSync,
rmSync,
statSync,
truncateSync,
writeFileSync,
} from "node:fs"
import { tmpdir } from "node:os"
import { dirname, join, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { z } from "zod"
import {
assertBinarySizeBudget,
assertEngineGraphBundled,
Expand Down Expand Up @@ -242,6 +245,29 @@ describe("runtime manifest", () => {
})

describe("size budget", () => {
test.each([
["darwin-arm64", 104_857_600, true],
["darwin-arm64", 104_857_601, false],
["linux-x64", 104_857_601, true],
["windows-x64", 157_286_400, true],
["windows-x64", 157_286_401, false],
] as const)("#given %s at %d bytes #when its target budget is enforced #then accepted is %s", (target, size, accepted) => {
// given
const root = makeTempDir("omo-size-boundary-")
const binary = join(root, "binary")
try {
writeFileSync(binary, "")
truncateSync(binary, size)
// when
const enforce = (): void => assertBinarySizeBudget(target, binary)
// then
if (accepted) expect(enforce).not.toThrow()
else expect(enforce).toThrow(new RegExp(target))
} finally {
rmSync(root, { recursive: true, force: true })
}
})

test("#given a synthetic oversize binary #when the budget is enforced #then it fails loud naming the target", () => {
// given
const stageDir = makeTempDir("omo-size-")
Expand Down Expand Up @@ -387,6 +413,45 @@ describe("plugin staging isolation guard", () => {
})

describe("engine graph bundling", () => {
test("#given a release build #when it reaches the compiler #then flags and both ordered entries satisfy the contract", () => {
// given: intercept only the target compiler; staging and the asset probe run normally.
const root = makeTempDir("omo-compile-argv-")
const capture = join(root, "argv.json")
try {
// when
const result = spawnSync(process.execPath, [join(scriptDir, "release-compile-argv.fixture.ts"), capture, root], {
cwd: repoRoot, encoding: "utf8", timeout: 120_000,
})
// then: independent flags may move; only entry order determines the executable's main.
expect(result.status, result.stderr).toBe(0)
const { command, args } = z.object({ command: z.string(), args: z.array(z.string()) }).parse(JSON.parse(readFileSync(capture, "utf8")))
expect(command).toBe("bun")
expect(args[0]).toBe("build")
for (const flag of ["--compile", "--target=bun-linux-x64", "--splitting", "--minify", "--keep-names", "--compile-autoload-package-json", "--no-compile-autoload-dotenv", "--no-compile-autoload-bunfig"]) {
expect(args).toContain(flag)
}
expect(args).not.toContain("--minify-whitespace")
const main = args.indexOf(join(repoRoot, "packages/omo-native/compile-entry.ts"))
const worker = args.indexOf(realpathSync(join(repoRoot, "node_modules/@code-yeongyu/senpi/dist/modes/rpc/session-worker.js")))
expect(main).toBeGreaterThanOrEqual(0)
expect(worker).toBeGreaterThanOrEqual(0)
expect(main).toBeLessThan(worker)
expect(args).toContain(`--root=${repoRoot}`)
expect(args.some((arg) => arg.startsWith("--define=SENPI_RPC_SESSION_WORKER_ENTRY="))).toBe(true)
expect(args.some((arg) => arg.startsWith("--asset="))).toBe(true)
} finally {
rmSync(root, { recursive: true, force: true })
}
}, 150_000)

test("#given recorded splitting output #when parsed #then the observed 4476 modules are extracted", () => {
// given: recorded two-entry splitting probe; not a production count pin.
const output = "\n [300ms] bundle 4476 modules\n\n [132ms] compile /tmp/x\n"
// when / then
expect(parseBundledModuleCount(output)).toBe(4476)
expect(assertEngineGraphBundled(output)).toBe(4476)
})

test("#given the compiled OMO entry #when its engine imports are inspected #then both retain the standard patched engine literal", () => {
// given
const compileEntrySource = readFileSync(
Expand Down
12 changes: 8 additions & 4 deletions script/build-omo-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ const compileEntry = join(repoRoot, "packages", "omo-native", "compile-entry.ts"
export const EMBEDDED_PAYLOAD_ROOT = "omo-runtime"
/** Relative path of the embedded runtime manifest inside the payload root. */
export const RUNTIME_MANIFEST_REL_PATH = "runtime-manifest.json"
/** Hard per-binary size budget (150MB). */
/** Hard per-binary size budget (150 MiB). */
export const MAX_BINARY_BYTES = 150 * 1024 * 1024
/** P0 release budget for the measured darwin-arm64 target (100 MiB). */
export const P0_MAX_BINARY_BYTES = 104_857_600

export interface ReleaseBinaryTarget {
/** Release asset platform slug, e.g. `darwin-arm64`. */
Expand Down Expand Up @@ -204,7 +206,7 @@ export function assertBinarySizeBudget(
binaryPath: string,
options: { readonly maxBytes?: number } = {},
): void {
const maxBytes = options.maxBytes ?? MAX_BINARY_BYTES
const maxBytes = options.maxBytes ?? (target === "darwin-arm64" ? P0_MAX_BINARY_BYTES : MAX_BINARY_BYTES)
const size = statSync(binaryPath).size
if (size > maxBytes) {
throw new Error(
Expand Down Expand Up @@ -625,7 +627,7 @@ export async function buildReleaseBinary(
)
}

// Flags mirror senpi's own scripts.build:binary (node_modules/@code-yeongyu/senpi/package.json).
// Split the shared engine graph while preserving runtime function/class names.
// A binary that fails post-compile verification must not survive on disk.
let compileOutput: string
try {
Expand All @@ -634,8 +636,10 @@ export async function buildReleaseBinary(
[
"build",
"--compile",
"--splitting",
`--target=${target.bunTarget}`,
"--minify-whitespace",
"--minify",
"--keep-names",
"--compile-autoload-package-json",
"--no-compile-autoload-dotenv",
"--no-compile-autoload-bunfig",
Expand Down
Loading
Loading