mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* fix(windows): restrict secure files in-process instead of spawning icacls Every secure write on Windows spawned icacls.exe five times on the calling thread (the Electron main thread for most stores): verify, /reset, /inheritance:r /grant:r, verify, verify. Process creation under endpoint security costs ~0.6 s each, so each write froze the UI for seconds (#26027). The /reset + /inheritance:r pair is also what behavioural EDR scores as ransomware tradecraft (#25583). Add native/windows-security (@orca/windows-security): a small N-API addon over advapi32 that reads and writes DACLs in-process. It exposes the token user SID, a DACL read as SDDL, a DACL write (sync and on the thread pool), a create-with-DACL file write, and an additive grant. Policy and verification stay in TypeScript. - writeSecureFile creates each temp file already carrying the protected user/SYSTEM/Administrators DACL (CreateFileW + SECURITY_ATTRIBUTES), reads it back once, and renames it. No ACL is modified on the write path and no process starts. Before: 5-7 spawns, 94-138 ms per write. After: 0 spawns, ~0.5 ms. - A secure directory is verified once per process. It is rewritten only when verification fails, on the thread pool, because the write walks every descendant (~2 s on a 29k-entry userData). - The verifier follows OpenSSH's rule: only the user, SYSTEM and Administrators may be granted anything, and the path must be protected (or, for a file, inherit from a secure parent). DACLs written by the icacls-era hardening pass unchanged, so there is no migration. - If the addon cannot load, the write still happens and reports unrestricted, logged once. There is no icacls or PowerShell fallback. - grantDirAcl, the startup userData grant, and the install-dir probe and package repair use the same addon, so whoami.exe and icacls.exe are no longer spawned anywhere at runtime. - orcad's Windows slots ship the addon beside their bundles (built per arch, PE-machine checked). Its data-root privacy check fails closed, so without the addon a Windows orcad would not start. Fixes #26027 Fixes #25583 * refactor(secure-file): harden existing Windows files through the sync lane The read path called restrictWindowsPathSync directly and recorded success itself; the existing sync lane already does both. One call site fewer. * refactor(windows-security): one load, one rule, no fallthrough (review #28135) - The descriptor parser is anchored, so a conditional ACE (its body holds parentheses) or anything else it cannot read fails closed instead of verifying the ACEs before it. - findSecureDaclViolation is one flat rule: protected, allow rules only for the user, SYSTEM and Administrators (OpenSSH's ACE rule), and full control for the user. The inherits-from-a-secure-parent exception is gone: it accepted exactly what loosening the parent reaches. - The addon and the user SID load once, behind one wrapper that adds the extended-length prefix to every path and runs async DACL writes to one path in order, so a startup grant cannot land after a re-lock and restore the broader DACL. - With the addon loaded, a failed born-restricted create throws with the matching Node error code instead of falling back to a plain write. - A grant leaves a NULL DACL untouched instead of turning 'everyone' into 'only this SID'. WriteFile runs once and fails on a short write. - The artifact create-intent store writes through writeSecureFile and drops its own temp writer, directory LRU and synchronous directory re-lock. - Test seams the global fake makes redundant are gone. * fix(packaging): fail a Windows build whose workspace addon binary is missing or foreign-arch The @orca/windows-* workspace links exist on every host, so their presence proved nothing: a missing or wrong-architecture .node only surfaced at runtime, as unrestricted secure files or an orcad that refuses to start. beforePack now reads each addon's PE machine against the target arch. * refactor(windows-security): prune what the native ACL left dead (review #28135) - Sync-spawn ratchet: hardenSecurePath and the sync-flag exemption it alone used are gone, and the advice and baseline header no longer say secure writers run icacls. - The install-dir probe reads ACEs through parseSddlDacl, so an ACE it cannot parse fails the whole reading closed instead of being skipped; AC is a resolved alias there now. - The packaging check reads the addon binary through node_modules, the copy that ships. - No winCode-to-errno table: nothing reads .code off a failed secure write. - The load memo is not mutated; secure-path keeps its own log-once flag. - grantInstallDirPackageAcl is a plain function, and secure-file restricts a Windows directory and a POSIX path directly instead of through one two-platform helper. * fix(windows-security): resolve LA in real-ACL tests; tear a file whose close fails - The GitHub Windows runner is the built-in Administrator (RID 500), whose SID reads back as LA. Production already resolves it through localDomainSidOf; the win32 test helpers parsed without it. One shared readDaclOf resolves aliases for every real-ACL suite, and the verifier's unit test covers an LA-aliased directory and LA as a stranger. - createFileWithDacl treats a failed CloseHandle like a failed write (lost data on a network volume) and deletes the torn file. - Drop a duplicate test reset and a typo. * fix(windows-security): load only from inside the app; read DACLs as structure (review #28135) - Loader: orcad's staged binary first, then the workspace package only if its real path is inside the app (process.resourcesPath, or this checkout). A bare specifier otherwise walks every parent up to C:\node_modules, which any local user may create, and orcad holds credentials. - The addon reads a DACL as { protected, aces: [{ type, flags, mask, sid }] } via GetAce, with a SID only for plain allow/deny ACEs and null for a NULL DACL. findSecureDaclViolation and the install-dir probe check that structure, so conditional, callback and object ACEs fail closed. The SDDL parser, its alias table, LA/localDomainSid resolution and getDaclSddl are gone; SDDL is only the write format. - Install-dir tree pass: files flagless as before, but a directory only when its own DACL is protected (inheritably). Re-granting inheriting directories re-propagated each subtree (3.2 s for 1,093 directories). The root is no longer granted, or counted, twice. The two passes are plain functions behind a { root, tree } seam. - One namespacing (path.win32.toNamespacedPath) at the addon boundary, also the per-path ordering key; the comment states the real sync-grant invariant. - restrictWindowsDirectory(dir): Promise<boolean> replaces the callback lane; the reporter is guarded once, so the 'settle' stage is gone. - Sync-spawn ratchet: the secure writers no longer spawn or block, so they leave the gate (109 -> 61 uses). - Errors name the Win32 call and code only; a failed GetSecurityDescriptorControl fails the write instead of defaulting to unprotected. * test(secure-file): assert the durable POSIX write's outcome, not the old chmod order writeSecureFile now chmods the temp file to 0o600 before fsyncing it, so the fsync also covers the mode change and the file is owner-writable when opened. Assert what matters under umask 0o200: the write succeeds, the temp file is fsynced read-only, and the published file is exactly 0o600. * fix(windows-security): namespace paths with the host's toNamespacedPath path.win32.toNamespacedPath rewrote POSIX temp paths (/tmp/x -> \tmp\x) in the Linux runs of the Windows-logic unit tests, so the fake addon wrote to the wrong place. The addon only runs on Windows, where the host function is the win32 one; elsewhere it is the identity. The long-path assertion runs on Windows only. * test(windows-security): assert the install-dir long-path prefix on Windows only The prefix comes from the host's toNamespacedPath, which is the identity off Windows; the Linux run of this suite expected the win32 escape. * fix(windows-user-data-acl): grant the root even when its children cannot be listed (review #28135) A readdir failure on userData aborted the whole grant, so the root grant that could repair a DACL refusing the listing was never attempted. The listing failure is now recorded, the root is still granted, and the marker stays withheld so the next launch retries. * test(windows-security): wait for relock propagation, accept Entra ID SIDs (review #28135) - The large-relock test waited only for the parent to turn protected, which happens before propagation reaches the children, then read a child. It now waits on the parent and both ends of the child range. - The token-user test rejected S-1-12-1 (Entra ID) accounts, failing the suite on Entra-joined dev machines. --------- Co-authored-by: Orca Worker <orca-worker@localhost>
548 lines
26 KiB
YAML
548 lines
26 KiB
YAML
name: Headless Node server
|
|
|
|
on:
|
|
pull_request:
|
|
types: [opened, synchronize, reopened, ready_for_review]
|
|
paths:
|
|
- 'src/**'
|
|
- 'config/**'
|
|
- 'native/**'
|
|
- 'tests/**'
|
|
- 'resources/**'
|
|
- 'package.json'
|
|
- 'pnpm-lock.yaml'
|
|
- 'pnpm-workspace.yaml'
|
|
- 'tsconfig.json'
|
|
- '.npmrc'
|
|
- '.pnpmfile.cjs'
|
|
- '.github/actions/install-node-dependencies/**'
|
|
- '.github/actions/restore-pnpm-verification/**'
|
|
- '.github/actions/prepare-headless-compiler/**'
|
|
- '.github/actions/prepare-native-runtime/**'
|
|
- '.github/actions/prepare-orcad-prebuilds/**'
|
|
- '.github/workflows/node-server-tests.yml'
|
|
# Relevant main pushes qualify every platform after the dependency check.
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'src/**'
|
|
- 'config/**'
|
|
- 'native/**'
|
|
- 'tests/**'
|
|
- 'resources/**'
|
|
- 'package.json'
|
|
- 'pnpm-lock.yaml'
|
|
- 'pnpm-workspace.yaml'
|
|
- 'tsconfig.json'
|
|
- '.npmrc'
|
|
- '.pnpmfile.cjs'
|
|
- '.github/actions/install-node-dependencies/**'
|
|
- '.github/actions/restore-pnpm-verification/**'
|
|
- '.github/actions/prepare-headless-compiler/**'
|
|
- '.github/actions/prepare-native-runtime/**'
|
|
- '.github/actions/prepare-orcad-prebuilds/**'
|
|
- '.github/workflows/node-server-tests.yml'
|
|
workflow_dispatch:
|
|
inputs:
|
|
build_template:
|
|
description: Also merge every lane's slot into the desktop orcad template artifact
|
|
type: boolean
|
|
default: false
|
|
# Release packaging calls this to build the orcad template it ships (design D2).
|
|
workflow_call:
|
|
inputs:
|
|
ref:
|
|
description: Git ref every lane checks out, e.g. the release tag
|
|
type: string
|
|
default: ''
|
|
build_template:
|
|
description: Upload each lane's release slot and merge them into the orcad-template artifact
|
|
type: boolean
|
|
default: false
|
|
schedule:
|
|
- cron: '30 11 * * *'
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# Main pushes must finish detection before they can supersede relevant qualification.
|
|
concurrency:
|
|
group: node-server-${{ (inputs.build_template || github.event_name == 'push') && format('run-{0}', github.run_id) || github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: ${{ !inputs.build_template && github.event_name != 'push' }}
|
|
|
|
jobs:
|
|
changes:
|
|
if: >-
|
|
github.event_name == 'push' ||
|
|
(github.event_name == 'pull_request' && github.event.pull_request.draft != true)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
outputs:
|
|
should_run: ${{ steps.scope.outputs.should_run || steps.graph.outputs.should_run }}
|
|
qualification: ${{ steps.scope.outputs.qualification || steps.graph.outputs.qualification }}
|
|
runners: ${{ steps.scope.outputs.runners || steps.graph.outputs.runners }}
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 2
|
|
persist-credentials: false
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: package.json
|
|
package-manager-cache: false
|
|
- name: Detect headless-server build and test inputs
|
|
id: scope
|
|
shell: bash
|
|
env:
|
|
PUSH_BASE: ${{ github.event.before }}
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
run: |
|
|
if [ "$EVENT_NAME" = push ]; then
|
|
# Compare the entire push, including multi-commit pushes and removed files.
|
|
if git fetch --no-tags --depth=1 origin "$PUSH_BASE" &&
|
|
git diff --name-only --no-renames -z "$PUSH_BASE" HEAD > "$RUNNER_TEMP/node-server-changes" &&
|
|
git diff --name-only --no-renames --diff-filter=M -z "$PUSH_BASE" HEAD > "$RUNNER_TEMP/node-server-modified"; then
|
|
node config/scripts/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" --defer-graph --full-qualification --modified-paths "$RUNNER_TEMP/node-server-modified"
|
|
else
|
|
echo 'should_run=true' >> "$GITHUB_OUTPUT"
|
|
fi
|
|
exit 0
|
|
fi
|
|
# Compare the tested merge with its base, retaining both sides of renames.
|
|
if git diff --name-only --no-renames -z HEAD^1 HEAD > "$RUNNER_TEMP/node-server-changes" &&
|
|
git diff --name-only --no-renames --diff-filter=M -z HEAD^1 HEAD > "$RUNNER_TEMP/node-server-modified"; then
|
|
node config/scripts/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" --defer-graph --modified-paths "$RUNNER_TEMP/node-server-modified"
|
|
else
|
|
echo 'should_run=true' >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- uses: ./.github/actions/prepare-headless-compiler
|
|
id: compiler
|
|
if: steps.scope.outputs.graph_required == 'true'
|
|
continue-on-error: true
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
if: steps.scope.outputs.graph_required == 'true' && steps.compiler.outputs.available != 'true'
|
|
- name: Check the headless import graph
|
|
id: graph
|
|
if: steps.scope.outputs.graph_required == 'true'
|
|
shell: bash
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
run: |
|
|
if [ "$EVENT_NAME" = push ]; then
|
|
node config/scripts/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" --full-qualification --modified-paths "$RUNNER_TEMP/node-server-modified"
|
|
else
|
|
node config/scripts/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" --modified-paths "$RUNNER_TEMP/node-server-modified"
|
|
fi
|
|
|
|
persistence:
|
|
needs: changes
|
|
concurrency:
|
|
group: node-server-persistence-${{ matrix.os }}-${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' && github.ref || github.run_id }}
|
|
cancel-in-progress: ${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' }}
|
|
# Missing/failed detection runs the full matrix; manual runs remain unconditional.
|
|
# A draft carries no platform verdict; readiness re-triggers this workflow. Spelled against
|
|
# the event name so the push and schedule paths do not rest on a null property comparison.
|
|
if: >-
|
|
${{ !cancelled() && needs.changes.outputs.should_run != 'false' &&
|
|
(github.event_name != 'pull_request' || github.event.pull_request.draft != true) }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: ${{ fromJSON(needs.changes.outputs.runners || '["ubuntu-22.04","ubuntu-24.04-arm","macos-15","macos-15-intel","windows-2022","windows-11-arm"]') }}
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 20
|
|
env:
|
|
ORCA_BACKGROUND_LAUNCH: '1'
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
persist-credentials: false
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
with:
|
|
native-runtime: ${{ runner.os == 'Windows' && 'node' || 'none' }}
|
|
cache-pnpm-store: ${{ runner.os != 'Windows' }}
|
|
cache-pnpm-store-lookup-only: 'true'
|
|
# Design D7 upgrade and rollback: the last Bun orcad, built from a main commit that shipped
|
|
# it, beside this checkout's Node slot; the live-terminal hand-over skips once PROTOCOL_VERSION
|
|
# moves past the Bun daemon's. Install its pinned dependencies independently of this checkout.
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
if: runner.os == 'Linux'
|
|
with:
|
|
bun-version: 1.4.2
|
|
- name: Build the last Bun orcad for the cross-runtime tests
|
|
id: bun-orcad
|
|
background: true
|
|
shell: bash
|
|
env:
|
|
BUN_ORCAD_COMMIT: f4092c06d639ee13ad446261dcabc78b27a21fbc
|
|
run: |
|
|
if [ "$RUNNER_OS" != Linux ]; then exit 0; fi
|
|
git fetch --no-tags --depth=1 origin "$BUN_ORCAD_COMMIT"
|
|
git worktree add --detach "$RUNNER_TEMP/bun-orcad-source" "$BUN_ORCAD_COMMIT"
|
|
pnpm --dir "$RUNNER_TEMP/bun-orcad-source" install --frozen-lockfile --ignore-scripts
|
|
node "$RUNNER_TEMP/bun-orcad-source/config/scripts/build-orcad-bun.mjs" --out-dir "$RUNNER_TEMP/bun-orcad"
|
|
echo "slot=$RUNNER_TEMP/bun-orcad" >> "$GITHUB_OUTPUT"
|
|
echo "executable=$(command -v bun)" >> "$GITHUB_OUTPUT"
|
|
# Linux release slots come from the floor and Alpine lanes; this slot serves local tests.
|
|
- uses: ./.github/actions/prepare-orcad-prebuilds
|
|
id: orcad-prebuild
|
|
with:
|
|
resolve-windows-cache: >-
|
|
${{ !inputs.build_template && inputs.ref == '' &&
|
|
(github.event_name == 'pull_request' ||
|
|
(github.ref == 'refs/heads/main' && contains(fromJSON('["push","schedule","workflow_dispatch"]'), github.event_name))) }}
|
|
restore-windows-cache: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
|
|
# The template lane cross-compiles arm64 too, as it does for the process-table addon.
|
|
- uses: ./.github/actions/prepare-windows-cli-launcher
|
|
if: runner.os == 'Windows'
|
|
with:
|
|
arches: ${{ (inputs.build_template && matrix.os == 'windows-2022') && 'x64 arm64' || (runner.arch == 'ARM64' && 'arm64' || 'x64') }}
|
|
- run: pnpm build:orcad
|
|
- wait: bun-orcad
|
|
- run: pnpm test:node-server --artifact ${{ runner.os == 'Linux' && '--cross-runtime' || '' }}
|
|
env:
|
|
ORCA_BUN_ORCAD_SLOT: ${{ steps.bun-orcad.outputs.slot }}
|
|
BUN_EXECUTABLE: ${{ steps.bun-orcad.outputs.executable }}
|
|
- name: Build the host-target desktop server template
|
|
if: runner.os == 'Linux'
|
|
run: node config/scripts/build-orcad-template.mjs --targets linux-${{ runner.arch == 'ARM64' && 'arm64' || 'x64' }}-glibc
|
|
- name: Verify a structured chat from the desktop server template
|
|
if: runner.os == 'Linux'
|
|
run: pnpm test:node-server --artifact src/main/orcad/orcad-packaged-structured-chat.integration.test.ts --maxWorkers=2
|
|
# Only a Windows runner compiles it; arm64 cross-compiles here.
|
|
# Before the Node 18 check below: the build script imports TypeScript, which Node 18 cannot load.
|
|
# orcad's instance lock and stop proof read a PID's creation time through it; the x64 binary this
|
|
# runner loads must answer it, and answer nothing for a missing PID rather than a stale time.
|
|
- name: Build the Windows process-table addons for the desktop template
|
|
if: inputs.build_template && matrix.os == 'windows-2022'
|
|
shell: bash
|
|
run: |
|
|
node config/scripts/build-windows-process-tree-addon.mjs --arch=x64
|
|
node config/scripts/build-windows-process-tree-addon.mjs --arch=arm64
|
|
node -e "
|
|
const { assertWindowsProcessTreeCreationTime } = require('./config/scripts/windows-process-tree-creation-time.cjs')
|
|
const addon = require('./.build/windows-process-tree/x64/windows-process-tree.node')
|
|
assertWindowsProcessTreeCreationTime({ module: addon })
|
|
const own = addon.getProcessCreationTime(process.pid)
|
|
const started = Date.now() - process.uptime() * 1000
|
|
if (typeof own !== 'number' || Math.abs(own - started) > 5000) throw new Error('creation time ' + own + ' vs ' + started)
|
|
if (addon.getProcessCreationTime(0x7ffffff0) !== undefined) throw new Error('a missing PID must read undefined')
|
|
console.log('creation time ok:', own)
|
|
"
|
|
- name: Keep the Windows process-table addons for the desktop template
|
|
if: inputs.build_template && matrix.os == 'windows-2022'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: orcad-windows-process-tree
|
|
path: .build/windows-process-tree/
|
|
include-hidden-files: true
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
overwrite: true
|
|
- name: Keep the Windows CLI launchers for the desktop template
|
|
if: inputs.build_template && matrix.os == 'windows-2022'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: orcad-windows-cli-launcher
|
|
path: .build/windows-cli-launcher/
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
overwrite: true
|
|
# orcad's data-root privacy check fails closed without it, so every Windows slot needs one.
|
|
- name: Build the Windows security addons for the desktop template
|
|
if: inputs.build_template && matrix.os == 'windows-2022'
|
|
shell: bash
|
|
run: |
|
|
node config/scripts/build-windows-security-addon.mjs --arch=x64
|
|
node config/scripts/build-windows-security-addon.mjs --arch=arm64
|
|
node -e "require('./.build/windows-security/x64/windows-security.node').currentUserSid()"
|
|
- name: Keep the Windows security addons for the desktop template
|
|
if: inputs.build_template && matrix.os == 'windows-2022'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: orcad-windows-security
|
|
path: .build/windows-security/*/windows-security.node
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
overwrite: true
|
|
- uses: actions/setup-node@v6
|
|
if: runner.arch == 'X64'
|
|
with:
|
|
node-version: '18'
|
|
- name: Verify Node 18 loads and hands off to the pinned Node
|
|
if: runner.arch == 'X64'
|
|
shell: bash
|
|
run: |
|
|
node out/orcad/orcad.js --orcad-smoke-load-check
|
|
node out/orcad/orcad.js --orcad-profile-state-preflight 00000000-0000-4000-8000-000000000018 | tee "$RUNNER_TEMP/preflight.json"
|
|
node -e "const r=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));if(r.runtime!=='node'||!/^24\./.test(r.runtimeVersion))process.exit(1)" "$RUNNER_TEMP/preflight.json"
|
|
- name: Save the freshly qualified Windows server prebuild on main
|
|
if: >-
|
|
success() && runner.os == 'Windows' && (runner.arch == 'X64' || runner.arch == 'ARM64') &&
|
|
!inputs.build_template && inputs.ref == '' && github.ref == 'refs/heads/main' &&
|
|
contains(fromJSON('["push","schedule","workflow_dispatch"]'), github.event_name) &&
|
|
steps.orcad-prebuild.outputs.cache-identity-outcome == 'success' &&
|
|
steps.orcad-prebuild.outputs.cache-key != ''
|
|
continue-on-error: true
|
|
uses: actions/cache/save@v5
|
|
with:
|
|
path: ${{ steps.orcad-prebuild.outputs.cache-path }}
|
|
key: ${{ steps.orcad-prebuild.outputs.cache-key }}
|
|
# Linux release slots come from the floor and Alpine lanes; these runners own the rest.
|
|
- name: Keep this runner's qualified slot for the desktop template
|
|
if: inputs.build_template && runner.os != 'Linux'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: orcad-prebuild-${{ matrix.os }}
|
|
path: out/orcad-prebuilds/
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
# A rerun attempt re-uploads under the same name, which v4 otherwise refuses.
|
|
overwrite: true
|
|
|
|
linux_glibc_floor:
|
|
needs: [changes, persistence]
|
|
concurrency:
|
|
group: node-server-linux_glibc_floor-${{ matrix.os }}-${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' && github.ref || github.run_id }}
|
|
cancel-in-progress: ${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' }}
|
|
# A failed smoke already blocks qualification; missing scope still selects every platform.
|
|
if: >-
|
|
${{ !cancelled() && needs.persistence.result == 'success' &&
|
|
needs.changes.outputs.should_run != 'false' &&
|
|
needs.changes.outputs.qualification != 'false' }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-22.04, ubuntu-24.04-arm]
|
|
# Design D6: server glibc slots are built and gated on glibc 2.28, where the pinned Node's
|
|
# own floor is; the desktop keeps its Ubuntu 20.04 gate. gcc-toolset-14 supplies C++20 and
|
|
# links newer libstdc++ symbols statically, so the slot needs only RHEL 8's GLIBCXX_3.4.25.
|
|
include:
|
|
- os: ubuntu-22.04
|
|
image: quay.io/pypa/manylinux_2_28_x86_64@sha256:407f771c51a2c3e83ebe5a7970b4289ead3a6db21d9b9c089168775cad11d328
|
|
- os: ubuntu-24.04-arm
|
|
image: quay.io/pypa/manylinux_2_28_aarch64@sha256:c22ffd129ac99a8a42d1f2c2f4e88a9089288dd9ee987a7092da1c7dc48f27a9
|
|
runs-on: ${{ matrix.os }}
|
|
container: ${{ matrix.image }}
|
|
timeout-minutes: 25
|
|
env:
|
|
ORCA_BACKGROUND_LAUNCH: '1'
|
|
# The image puts /opt/clang first on PATH; the slot is built with its gcc-toolset.
|
|
CC: gcc
|
|
CXX: g++
|
|
PYTHON: /opt/python/cp312-cp312/bin/python3
|
|
steps:
|
|
- name: Install glibc 2.28 prerequisites
|
|
run: |
|
|
missing_tool=false
|
|
for tool in git ps unzip which xz; do
|
|
if ! command -v "$tool" >/dev/null 2>&1; then
|
|
missing_tool=true
|
|
fi
|
|
done
|
|
if [ "$missing_tool" = true ]; then
|
|
# The image's source-built Git needs no RPM; missing tools come from AlmaLinux.
|
|
dnf --disablerepo='epel*' install -y git procps-ng unzip which xz
|
|
fi
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
persist-credentials: false
|
|
- name: Trust the checked-out workspace
|
|
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
- name: Build and smoke this runner's node-pty prebuild slot under the pinned Node
|
|
run: |
|
|
pnpm build:orcad-prebuilds
|
|
pnpm build:orcad-prebuilds --require-slots "$(node config/scripts/build-orcad-prebuilds.mjs --print-slot)"
|
|
pnpm build:orcad-prebuilds --smoke
|
|
- run: pnpm build:orcad
|
|
- run: pnpm test:node-server --artifact
|
|
- name: Keep this runner's glibc 2.28 slot for the desktop template
|
|
if: inputs.build_template
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: orcad-prebuild-glibc-${{ matrix.os }}
|
|
path: out/orcad-prebuilds/
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
overwrite: true
|
|
|
|
linux_glibc217_compat:
|
|
needs: [changes, persistence]
|
|
concurrency:
|
|
group: node-server-linux_glibc217_compat-${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' && github.ref || github.run_id }}
|
|
cancel-in-progress: ${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' }}
|
|
# A failed smoke already blocks qualification; missing scope still selects every platform.
|
|
if: >-
|
|
${{ !cancelled() && needs.persistence.result == 'success' &&
|
|
needs.changes.outputs.should_run != 'false' &&
|
|
needs.changes.outputs.qualification != 'false' }}
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 20
|
|
env:
|
|
ORCA_BACKGROUND_LAUNCH: '1'
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
persist-credentials: false
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
# Design D6 rung B: the opt-in linux-x64-glibc217 slot beside the unofficial glibc-217 Node.
|
|
# Why docker run and not container: the runner's own Node for JS actions needs glibc 2.28,
|
|
# so the host installs and fetches the pinned Node, and the glibc 2.17 image only builds and
|
|
# smokes with that Node. devtoolset-10 supplies C++20; the slot links libstdc++ statically.
|
|
- name: Build and smoke the glibc 2.17 compat slot under the glibc-217 Node
|
|
run: |
|
|
compat_node="$(node config/scripts/build-orcad-prebuilds.mjs --slot=linux-x64-glibc217 --print-runtime)"
|
|
case "$compat_node" in
|
|
"$GITHUB_WORKSPACE"/*) ;;
|
|
*) echo "glibc-217 Node cached outside the workspace: $compat_node" >&2; exit 1 ;;
|
|
esac
|
|
docker run --rm --init -i \
|
|
-e ORCA_BACKGROUND_LAUNCH=1 \
|
|
-e COMPAT_NODE="/work/${compat_node#"$GITHUB_WORKSPACE"/}" \
|
|
-e CC=gcc -e CXX=g++ \
|
|
-e PYTHON=/opt/python/cp312-cp312/bin/python3 \
|
|
-v "$GITHUB_WORKSPACE:/work" -w /work \
|
|
quay.io/pypa/manylinux2014_x86_64@sha256:6f74cabeac2432570aa4bfdb29f7c1f30313d4d6654d764c44e574b6ffdd4ed5 bash -s <<'GLIBC217_COMPAT_SLOT'
|
|
set -eu
|
|
export PATH="$(dirname "$COMPAT_NODE"):$PATH"
|
|
node config/scripts/build-orcad-prebuilds.mjs --slot=linux-x64-glibc217
|
|
node config/scripts/build-orcad-prebuilds.mjs --require-slots linux-x64-glibc217
|
|
# The image's devtoolset LD_LIBRARY_PATH must not stand in for a host C++ runtime.
|
|
env -u LD_LIBRARY_PATH node config/scripts/build-orcad-prebuilds.mjs --slot=linux-x64-glibc217 --smoke
|
|
GLIBC217_COMPAT_SLOT
|
|
- name: Keep the glibc 2.17 compat slot for the desktop template
|
|
if: inputs.build_template
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: orcad-prebuild-glibc217
|
|
path: out/orcad-prebuilds/
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
overwrite: true
|
|
|
|
linux_musl:
|
|
needs: [changes, persistence]
|
|
concurrency:
|
|
group: node-server-linux_musl-${{ matrix.os }}-${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' && github.ref || github.run_id }}
|
|
cancel-in-progress: ${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' }}
|
|
# A failed smoke already blocks qualification; missing scope still selects every platform.
|
|
if: >-
|
|
${{ !cancelled() && needs.persistence.result == 'success' &&
|
|
needs.changes.outputs.should_run != 'false' &&
|
|
needs.changes.outputs.qualification != 'false' }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-22.04, ubuntu-24.04-arm]
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 20
|
|
env:
|
|
ORCA_BACKGROUND_LAUNCH: '1'
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
persist-credentials: false
|
|
- uses: ./.github/actions/restore-pnpm-verification
|
|
id: pnpm-verification
|
|
with:
|
|
container-toolchain: 'true'
|
|
- name: Verify native Alpine artifact and persistence
|
|
env:
|
|
VERIFICATION_CACHE_PATH: ${{ steps.pnpm-verification.outputs.path }}
|
|
run: |
|
|
touch "$VERIFICATION_CACHE_PATH"
|
|
# Multi-arch index digest of the tag; re-resolve it whenever NODE_RUNTIME_PIN moves.
|
|
docker run --rm --init -i \
|
|
-e ORCA_BACKGROUND_LAUNCH=1 \
|
|
-v "$VERIFICATION_CACHE_PATH:/root/.cache/pnpm/lockfile-verified.jsonl" \
|
|
-v "$GITHUB_WORKSPACE:/work" -w /work \
|
|
node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1 sh -s <<'NODE_SERVER_QUALIFICATION'
|
|
set -eu
|
|
apk add --no-cache bash git libstdc++ python3 make g++
|
|
git config --global --add safe.directory /work
|
|
npm install -g "$(node -p "require('./package.json').packageManager.split('+')[0]")"
|
|
pnpm install --frozen-lockfile --ignore-scripts
|
|
pnpm build:orcad-prebuilds
|
|
pnpm build:orcad-prebuilds --require-slots "$(node config/scripts/build-orcad-prebuilds.mjs --print-slot)"
|
|
pnpm build:orcad-prebuilds --smoke
|
|
pnpm build:orcad
|
|
pnpm test:node-server --artifact
|
|
NODE_SERVER_QUALIFICATION
|
|
- name: Keep this runner's musl slot for the desktop template
|
|
if: inputs.build_template
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: orcad-prebuild-musl-${{ matrix.os }}
|
|
path: out/orcad-prebuilds/
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
overwrite: true
|
|
|
|
# Design D2: the desktop ships every target's addons, merged from the lanes that qualified them.
|
|
desktop_template:
|
|
needs: [persistence, linux_glibc_floor, linux_glibc217_compat, linux_musl]
|
|
if: >-
|
|
${{ !cancelled() && inputs.build_template &&
|
|
needs.persistence.result == 'success' && needs.linux_glibc_floor.result == 'success' &&
|
|
needs.linux_glibc217_compat.result == 'success' && needs.linux_musl.result == 'success' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
persist-credentials: false
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
- name: Collect every lane's slot
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: orcad-prebuild-*
|
|
path: ${{ runner.temp }}/orcad-prebuild-lanes
|
|
- name: Collect the Windows process-table addons
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: orcad-windows-process-tree
|
|
path: .build/windows-process-tree
|
|
- name: Collect the Windows CLI launchers
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: orcad-windows-cli-launcher
|
|
path: .build/windows-cli-launcher
|
|
- name: Collect the Windows security addons
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: orcad-windows-security
|
|
path: .build/windows-security
|
|
- name: Merge the lanes and gate the full slot matrix
|
|
shell: bash
|
|
run: |
|
|
node config/scripts/merge-orcad-prebuilds.mjs "$RUNNER_TEMP"/orcad-prebuild-lanes/*
|
|
pnpm build:orcad-prebuilds --require-slots
|
|
pnpm build:orcad-prebuilds --require-slots linux-x64-glibc217
|
|
- run: pnpm build:orcad-template
|
|
- name: Report the template size
|
|
shell: bash
|
|
run: |
|
|
{
|
|
echo '### orcad template'
|
|
echo '```'
|
|
du -sh out/orcad-template
|
|
du -sh out/orcad-template/targets/*
|
|
echo '```'
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: orcad-template
|
|
path: out/orcad-template/
|
|
# The per-target .server-target and .runtime-node markers are dotfiles.
|
|
include-hidden-files: true
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
overwrite: true
|