Files
orca/.github/workflows/node-server-tests.yml
T
Jinjing a8f6e79631 Improve workspace sidebar cards and activity status (#41894)
* wip

* wip

* wip

* wip

* Improve experimental worktree cards with compact agent summaries

- Add an optional details row for workspace metadata and indicators
- Roll up descendant activity timestamps to parent cards

* Reduce worktree card metadata indentation

* wip

* Expand sidebar agent lines with subagents and remote host status

- Show subagent activity, send targets, and unread signals in slot cards
- Identify remote hosts and provide SSH reconnection controls

* Show child workspace status counts in sidebar cards

Distinguish monitoring agents and refine lineage alignment so workspace
activity is easier to scan.

* Enable navigation from worktree sidebar activity indicators

- Open the matching agent or workspace directly, or choose among multiple sources
- Use host-qualified identities for nested workspace activity
- Show pull requests in 2.0 card details and refine compact card layout

* Show every status in the sidebar card corner and let cards open and close

The title-row corner now shows one glyph per status the card speaks for
(needs, failed, unread, working), most urgent first, followed by the time,
instead of only the most urgent one. Each glyph opens its own sources.
Collapsed child workspaces report all their statuses through a flat,
shallow-comparable record.

Experimental 2.0 cards can also be opened and closed; a closed card's
title slot speaks for its hidden agents and children.

* final worktree design

* wip

* rm

* wip

* wip

* fix style

* fix commit

* Improve worktree card interactions and spacing

- Let users toggle cards by clicking their headers.
- Delay details until the pointer rests to avoid accidental popups.
- Add breathing room around card content.

* wip

* fix issues

* wip

* Improve workspace sidebar icons and navigation

- Batch AI icon generation and add settings to customize and regenerate icons.
- Fix agent navigation and unread handling across execution hosts.
- Keep linked reviews visible while loading and reduce unnecessary refresh work.

* Fix sidebar agent ownership and targeted title updates

Resolve unstamped agent sessions to their workspace runtime so sidebar
summaries and unread state follow the correct host. Limit title-change
invalidation to affected workspaces.

* fix build

* Simplify worktree sidebar icons and agent rows

- Generate monochrome icons that match the sidebar foreground
- Move agent model names into row tooltips to reduce clutter

* Keep agent timestamps visible alongside dismiss controls

* Fix worktree icon regeneration and bound the cache

Cancel stale requests when clearing icons so older results cannot
restore the previous cache. Evict unused renderer entries to limit
memory growth.

* Unify experimental workspace cards around title status

- Surface hidden agent and child workspace activity in the card title.
- Retire the second experimental layout and AI-generated icons.
- Preserve existing experimental card selections and add a details row option.

* wip

* wip

* wip

* fix bugs

* improve perf

* Fix sidebar lineage rollup: unread dot on children, running status on collapsed parent

- A card's unread dot and bold title now include its own visible agent rows'
  unread results, so expanding a parent reveals which child the unread came from.
- A collapsed parent's status lane shows the most urgent descendant state
  (needs you > failed > working) when it beats the parent's own status.
- Host-scoped workspace unread markers and related attention fixes.

* Improve sidebar activity rollup performance

- Refresh only stale descendant slots and scope unread updates to their workspace.
- Share agent row projections and index unread markers to reduce repeated work.
- Preserve host-specific agent activation and show the latest activity time.

* wip

* Fix sidebar attention state and optimize slot invalidation

- Preserve unread flags raised by users or earlier sessions when markers clear.
- Scope agent list expansion to each host.
- Track displayed panes and freshness deadlines to limit slot updates.

* wip

* Fix sidebar workspace status and host ownership

- Keep agent navigation and unread state scoped to the correct host,
  including paired SSH workspaces and runtimes.
- Correct collapsed lineage status, stale review display, and settings
  synchronization while reducing unnecessary sidebar refreshes.

* Show unread agent completions and scope workspace reads by host

* fix compile issue

* wip fix static checks

* wip

* Fix workspace sidebar agent ownership and PR display

- Update agent rows when terminal host bindings arrive and preserve
  local ownership for unstamped retained completions.
- Keep newer hosted PRs visible when an older merged PR is cached.
2026-11-14 02:41:25 +00:00

694 lines
36 KiB
YAML

name: Headless Node server
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
paths:
- 'src/**'
- 'config/**'
- 'native/**'
- 'tests/**'
- 'resources/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'tsconfig.json'
- '.npmrc'
- 'config/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
fast_runners:
description: Use Blacksmith where the platform and architecture are available
type: boolean
default: true
# Release and dev builds qualify every platform for the template they ship.
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
fast_runners:
description: Use Blacksmith where the platform and architecture are available
type: boolean
default: true
# Requalify fresh builds daily; publish missing immutable Windows cache keys.
schedule:
- cron: '30 11 * * *'
permissions:
contents: read
# Keep template callers independent while replacing superseded PR 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: ${{ inputs.fast_runners && 'blacksmith-2vcpu-ubuntu-2404' || '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 }}
native_compatibility: ${{ steps.scope.outputs.native_compatibility || steps.graph.outputs.native_compatibility }}
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
check-latest: true
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 }}
ALLOW_TEST_ONLY_NATIVE_SKIP: ${{ github.event_name == 'pull_request' && !inputs.build_template && inputs.ref == '' }}
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/qualification/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
NATIVE_SCOPE_ARGS=()
if [ "$ALLOW_TEST_ONLY_NATIVE_SKIP" = true ]; then
NATIVE_SCOPE_ARGS=(--pull-request --diff-paths "$RUNNER_TEMP/node-server-native-diff")
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" &&
git diff --raw --no-renames --no-abbrev -z HEAD^1 HEAD > "$RUNNER_TEMP/node-server-native-diff"; then
node config/scripts/node-server/qualification/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" --defer-graph --modified-paths "$RUNNER_TEMP/node-server-modified" "${NATIVE_SCOPE_ARGS[@]}"
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 }}
ALLOW_TEST_ONLY_NATIVE_SKIP: ${{ github.event_name == 'pull_request' && !inputs.build_template && inputs.ref == '' }}
run: |
if [ "$EVENT_NAME" = push ]; then
node config/scripts/node-server/qualification/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" --full-qualification --modified-paths "$RUNNER_TEMP/node-server-modified"
else
NATIVE_SCOPE_ARGS=()
if [ "$ALLOW_TEST_ONLY_NATIVE_SKIP" = true ]; then
NATIVE_SCOPE_ARGS=(--pull-request --diff-paths "$RUNNER_TEMP/node-server-native-diff")
fi
node config/scripts/node-server/qualification/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" --modified-paths "$RUNNER_TEMP/node-server-modified" "${NATIVE_SCOPE_ARGS[@]}"
fi
persistence:
needs: [changes, mobile_web_bundle]
permissions:
contents: read
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; scheduled and manual runs stay unconditional.
# Drafts re-trigger at readiness; inherited caller events must not depend on PR fields.
if: >-
${{ !cancelled() && needs.changes.outputs.should_run != 'false' &&
(needs.changes.outputs.qualification == 'false' || needs.mobile_web_bundle.result == 'success') &&
(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"]') }}
# Keep platform keys stable for slot names and Windows cross-compilation. PR and scheduled runs use the
# free hosted label; only explicit fast_runners callers rent Blacksmith where it has the platform.
runs-on: >-
${{ inputs.fast_runners && fromJSON('{"ubuntu-22.04":"blacksmith-8vcpu-ubuntu-2204","ubuntu-24.04-arm":"blacksmith-8vcpu-ubuntu-2404-arm","macos-15":"blacksmith-6vcpu-macos-15"}')[matrix.os] || matrix.os }}
timeout-minutes: 20
env:
ORCA_BACKGROUND_LAUNCH: '1'
JOBS: ${{ (inputs.fast_runners || github.event_name == 'pull_request' || github.event_name == 'schedule') && 'max' || '' }}
steps:
- name: Parallelize Windows checkout
if: (inputs.fast_runners || github.event_name == 'pull_request' || github.event_name == 'schedule') && runner.os == 'Windows'
shell: bash
run: |
git config --global checkout.workers 0
# These fresh native builds do not need MSBuild's incremental file tracking.
echo 'TrackFileAccess=false' >> "$GITHUB_ENV"
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
id: dependencies
with:
native-runtime: ${{ runner.os == 'Windows' && 'node' || 'none' }}
# Template qualification must not wait for large download-cache uploads.
cache-pnpm-store: ${{ !inputs.build_template && 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, using the current Bun pin beside this checkout's Node slot; hand-over skips once PROTOCOL_VERSION
# moves past the Bun daemon's. Install its pinned dependencies independently of this checkout.
- name: Find installed Bun
id: installed-bun
if: runner.os == 'Linux'
shell: bash
run: |
bun="$GITHUB_WORKSPACE/node_modules/.bin/bun"
if [ -x "$bun" ] && [ "$("$bun" --version)" = "$(cat config/.bun-version)" ]; then
echo "$GITHUB_WORKSPACE/node_modules/.bin" >> "$GITHUB_PATH"
echo 'found=true' >> "$GITHUB_OUTPUT"
fi
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
if: runner.os == 'Linux' && steps.installed-bun.outputs.found != 'true'
with:
bun-version-file: config/.bun-version
no-cache: true
- 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.fast_runners || (!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: ${{ inputs.fast_runners || 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') }}
- uses: actions/download-artifact@v8
id: mobile-web-artifact
if: needs.changes.outputs.qualification != 'false'
continue-on-error: true
with:
name: orcad-mobile-web-bundle
path: out/mobile-web
- name: Retry mobile web artifact download
if: steps.mobile-web-artifact.outcome == 'failure'
uses: actions/download-artifact@v8
with:
name: orcad-mobile-web-bundle
path: out/mobile-web
# Narrow test-only runs skip the shared producer but still need a page.
- uses: ./.github/actions/build-mobile-web-bundle
if: needs.changes.outputs.qualification == 'false'
- run: pnpm build:orcad
env:
ORCA_REUSE_PREPARED_WINDOWS_SECURITY: ${{ runner.os == 'Windows' && steps.dependencies.outcome == 'success' }}
- 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 "$(if [ -f config/scripts/orcad-distribution/template/build-orcad-template.mjs ]; then printf %s config/scripts/orcad-distribution/template/build-orcad-template.mjs; else printf %s config/scripts/build-orcad-template.mjs; fi)" --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/testing/structured-chat/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 "$(if [ -f config/scripts/native-build/windows/build-windows-process-tree-addon.mjs ]; then printf %s config/scripts/native-build/windows/build-windows-process-tree-addon.mjs; else printf %s config/scripts/build-windows-process-tree-addon.mjs; fi)" --arch=x64
node "$(if [ -f config/scripts/native-build/windows/build-windows-process-tree-addon.mjs ]; then printf %s config/scripts/native-build/windows/build-windows-process-tree-addon.mjs; else printf %s config/scripts/build-windows-process-tree-addon.mjs; fi)" --arch=arm64
node -e "
const { assertWindowsProcessTreeCreationTime } = require(require('node:fs').existsSync('./config/scripts/native-build/windows/windows-process-tree-creation-time.cjs') ? './config/scripts/native-build/windows/windows-process-tree-creation-time.cjs' : './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.
# Older requested refs use the previous security implementation and have no addon builder.
- name: Build the Windows security addons for the desktop template
if: inputs.build_template && matrix.os == 'windows-2022' && hashFiles('config/scripts/native-build/windows/build-windows-security-addon.mjs', 'config/scripts/build-windows-security-addon.mjs') != ''
shell: bash
run: |
node "$(if [ -f config/scripts/native-build/windows/build-windows-security-addon.mjs ]; then printf %s config/scripts/native-build/windows/build-windows-security-addon.mjs; else printf %s config/scripts/build-windows-security-addon.mjs; fi)" --arch=x64
node "$(if [ -f config/scripts/native-build/windows/build-windows-security-addon.mjs ]; then printf %s config/scripts/native-build/windows/build-windows-security-addon.mjs; else printf %s config/scripts/build-windows-security-addon.mjs; fi)" --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' && hashFiles('config/scripts/native-build/windows/build-windows-security-addon.mjs', 'config/scripts/build-windows-security-addon.mjs') != ''
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
if: >-
success() && runner.os == 'Windows' && (runner.arch == 'X64' || runner.arch == 'ARM64') &&
((inputs.fast_runners && github.event_name == 'workflow_dispatch') ||
(!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
# Every qualification lane tests the same platform-independent page.
mobile_web_bundle:
needs: changes
if: >-
${{ !cancelled() && needs.changes.outputs.should_run != 'false' &&
needs.changes.outputs.qualification != 'false' &&
(github.event_name != 'pull_request' || github.event.pull_request.draft != true) }}
runs-on: ${{ inputs.fast_runners && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
- uses: ./.github/actions/build-mobile-web-bundle
- uses: actions/upload-artifact@v7
with:
name: orcad-mobile-web-bundle
path: out/mobile-web/
if-no-files-found: error
retention-days: 1
overwrite: true
linux_glibc_floor:
needs: [changes, mobile_web_bundle]
permissions:
contents: read
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 == '' }}
# Each Linux lane owns its inputs; the template still requires every lane to pass.
if: >-
${{ !cancelled() &&
needs.changes.outputs.should_run != 'false' &&
needs.changes.outputs.qualification != 'false' &&
needs.mobile_web_bundle.result == 'success' &&
(github.event_name != 'pull_request' || github.event.pull_request.draft != true) }}
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: ${{ inputs.fast_runners && (matrix.os == 'ubuntu-22.04' && 'blacksmith-8vcpu-ubuntu-2204' || 'blacksmith-8vcpu-ubuntu-2404-arm') || 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"
# A direct install (6-9s) beats restoring the 417 MB store plus a linking install.
- uses: ./.github/actions/install-node-dependencies
with:
cache-pnpm-store: 'false'
- 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 "$(if [ -f config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs ]; then printf %s config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs; else printf %s config/scripts/build-orcad-prebuilds.mjs; fi)" --print-slot)"
pnpm build:orcad-prebuilds --smoke
- uses: actions/download-artifact@v8
with:
name: orcad-mobile-web-bundle
path: out/mobile-web
- 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
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 == '' }}
if: >-
${{ !cancelled() &&
needs.changes.outputs.should_run != 'false' &&
needs.changes.outputs.qualification != 'false' &&
(github.event_name != 'pull_request' || github.event.pull_request.draft != true) &&
(github.event_name != 'pull_request' || inputs.build_template || inputs.ref != '' ||
needs.changes.outputs.native_compatibility != 'false') }}
runs-on: ${{ inputs.fast_runners && 'blacksmith-8vcpu-ubuntu-2204' || '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 "$(if [ -f config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs ]; then printf %s config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs; else printf %s config/scripts/build-orcad-prebuilds.mjs; fi)" --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 "$(if [ -f config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs ]; then printf %s config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs; else printf %s config/scripts/build-orcad-prebuilds.mjs; fi)" --slot=linux-x64-glibc217
node "$(if [ -f config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs ]; then printf %s config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs; else printf %s config/scripts/build-orcad-prebuilds.mjs; fi)" --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 "$(if [ -f config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs ]; then printf %s config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs; else printf %s config/scripts/build-orcad-prebuilds.mjs; fi)" --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, mobile_web_bundle]
permissions:
contents: read
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 == '' }}
if: >-
${{ !cancelled() &&
needs.changes.outputs.should_run != 'false' &&
needs.changes.outputs.qualification != 'false' &&
needs.mobile_web_bundle.result == 'success' &&
(github.event_name != 'pull_request' || github.event.pull_request.draft != true) }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, ubuntu-24.04-arm]
runs-on: ${{ inputs.fast_runners && (matrix.os == 'ubuntu-22.04' && 'blacksmith-8vcpu-ubuntu-2204' || 'blacksmith-8vcpu-ubuntu-2404-arm') || 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: Prepare native Alpine artifact
env:
VERIFICATION_CACHE_PATH: ${{ steps.pnpm-verification.outputs.path }}
run: |
touch "$VERIFICATION_CACHE_PATH"
mkdir -p out/mobile-web
# Multi-arch index digest of the tag; re-resolve it whenever NODE_RUNTIME_PIN moves.
docker run --rm --init --detach --name orcad-alpine \
-e ORCA_BACKGROUND_LAUNCH=1 \
-v "$VERIFICATION_CACHE_PATH:/root/.cache/pnpm/lockfile-verified.jsonl" \
-v "$GITHUB_WORKSPACE:/work" -w /work \
node:24.22.1-alpine@sha256:6a1c190e17215d844926b0677f4bf87273ff7d5e54db06d3b5b5863ab0b09a3d sleep infinity
docker exec -i orcad-alpine sh -s <<'NODE_SERVER_PREPARATION'
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 "$(if [ -f config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs ]; then printf %s config/scripts/orcad-distribution/prebuilds/build-orcad-prebuilds.mjs; else printf %s config/scripts/build-orcad-prebuilds.mjs; fi)" --print-slot)"
pnpm build:orcad-prebuilds --smoke
NODE_SERVER_PREPARATION
- uses: actions/download-artifact@v8
with:
name: orcad-mobile-web-bundle
path: out/mobile-web
- name: Verify native Alpine artifact and persistence
run: |
docker exec -i orcad-alpine sh -s <<'NODE_SERVER_QUALIFICATION'
set -eu
pnpm build:orcad
pnpm test:node-server --artifact
NODE_SERVER_QUALIFICATION
- name: Clean up the Alpine qualification container
if: always()
run: docker rm --force orcad-alpine
- 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: changes
if: ${{ !cancelled() && inputs.build_template }}
permissions:
contents: read
actions: read
runs-on: ${{ inputs.fast_runners && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
- name: Wait for every template qualification lane
uses: actions/github-script@v8
with:
script: |
const deadline = Date.now() + 25 * 60_000
const expected = {
persistence: 6,
mobile_web_bundle: 1,
linux_glibc_floor: 2,
linux_glibc217_compat: 1,
linux_musl: 2
}
while (Date.now() < deadline) {
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
...context.repo, run_id: context.runId, filter: 'latest', per_page: 100
})
const assemblers = jobs.filter(job =>
job.name === 'desktop_template' || job.name.endsWith(' / desktop_template'))
if (assemblers.length > 1) throw new Error('Ambiguous desktop template qualification')
if (assemblers.length === 1) {
const prefix = assemblers[0].name.slice(0, -'desktop_template'.length)
let ready = true
for (const [name, count] of Object.entries(expected)) {
const lanes = jobs.filter(job => job.name === `${prefix}${name}` ||
job.name.startsWith(`${prefix}${name} (`))
if (lanes.length > count) throw new Error(`Unexpected qualification lanes: ${name}`)
for (const lane of lanes) {
if (lane.status === 'completed' && lane.conclusion !== 'success') {
throw new Error(`Template qualification did not succeed: ${lane.name} (${lane.conclusion})`)
}
}
if (lanes.length !== count || lanes.some(lane => lane.status !== 'completed')) ready = false
}
if (ready) return
}
await new Promise(resolve => setTimeout(resolve, 15_000))
}
throw new Error('Timed out waiting for every template qualification lane')
- 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
if: hashFiles('config/scripts/native-build/windows/build-windows-security-addon.mjs', 'config/scripts/build-windows-security-addon.mjs') != ''
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 "$(if [ -f config/scripts/orcad-distribution/prebuilds/merge-orcad-prebuilds.mjs ]; then printf %s config/scripts/orcad-distribution/prebuilds/merge-orcad-prebuilds.mjs; else printf %s config/scripts/merge-orcad-prebuilds.mjs; fi)" "$RUNNER_TEMP"/orcad-prebuild-lanes/*
pnpm build:orcad-prebuilds --require-slots
pnpm build:orcad-prebuilds --require-slots linux-x64-glibc217
# The page the qualification lanes tested, not a rebuild.
- uses: actions/download-artifact@v8
with:
name: orcad-mobile-web-bundle
path: out/mobile-web
- 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