Keep restore-only and self-hosted stores outside pnpm/action-setup's replaceable install directory, resolving the configured path for every paired cache. Pin Yarn 4 for the generated-project e2e so runner images without Corepack do not fall back to Yarn Classic, and update the bilingual decision record.
991 lines
39 KiB
YAML
991 lines
39 KiB
YAML
name: CI
|
||
|
||
on:
|
||
push:
|
||
branches: [master]
|
||
pull_request:
|
||
workflow_dispatch:
|
||
inputs:
|
||
suite:
|
||
description: Manual CI suite to run
|
||
required: true
|
||
default: larger-runner-benchmark
|
||
type: choice
|
||
options:
|
||
- larger-runner-benchmark
|
||
- consolidated-runner-benchmark
|
||
|
||
concurrency:
|
||
group: ${{ github.workflow }}-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
env:
|
||
PRIMARY_NODE_VERSION: '24'
|
||
|
||
jobs:
|
||
|
||
# Three enterprise jobs isolate coverage, static analysis, and the
|
||
# build-backed consumer tail. The static job publishes its exact build so
|
||
# consumers do not repeat the longest part of their critical path.
|
||
#
|
||
# FAILOVER: each Linux enterprise job resolves its pool through the
|
||
# DSH_CI_FAILOVER repository variable. Unset (normal), the expressions
|
||
# pick the hosted enterprise pools below. Setting the variable to
|
||
# 'selfhosted' (repo Settings → Actions → Variables; writer-manageable
|
||
# repository state — not PR-editable, no merge required) retargets all
|
||
# three onto the in-house
|
||
# vm-backup pool and re-running the failed jobs is the entire switch —
|
||
# see .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md. The
|
||
# in-house pool's readiness is re-proven on every master push by the
|
||
# serial-linux-selfhosted standby lane below.
|
||
node-24:
|
||
if: github.event_name == 'pull_request'
|
||
runs-on: >-
|
||
${{ vars.DSH_CI_FAILOVER == 'selfhosted'
|
||
&& github.event.pull_request.user.login != 'dependabot[bot]'
|
||
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|
||
|| 'dsh-enterprise-ubuntu-latest-32core-test' }}
|
||
name: node 24 / static
|
||
env:
|
||
DSH_GATE_CONCURRENCY: '8'
|
||
steps:
|
||
# Fetch complete history so the archive gate can read the trusted PR base from a reused shallow checkout.
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
fetch-depth: 0
|
||
persist-credentials: false
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||
|
||
- name: Configure pnpm store path
|
||
id: pnpm-store
|
||
run: |
|
||
store_root="$HOME/.local/share/pnpm/store"
|
||
echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
|
||
store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
|
||
echo "path=$store_path" >> "$GITHUB_OUTPUT"
|
||
|
||
# Pull requests consume the default-branch cache but do not put cache
|
||
# compression and upload on the paid latency-critical path. Skipped
|
||
# under failover — see the coverage lane's identical rationale.
|
||
- uses: actions/cache/restore@v4
|
||
if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
|
||
with:
|
||
path: ${{ steps.pnpm-store.outputs.path }}
|
||
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
|
||
|
||
- name: Install (immutable)
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Run static gates
|
||
env:
|
||
DSH_ARCHIVE_BASE_REF: ${{ github.event.pull_request.base.sha }}
|
||
run: pnpm run check:ci:static
|
||
|
||
- name: Pack built tree
|
||
run: >-
|
||
tar -czf "$RUNNER_TEMP/node-24-built-tree.tar.gz"
|
||
apps/*/lib packages/*/*/lib vendor/*/lib
|
||
|
||
- uses: actions/upload-artifact@v6
|
||
with:
|
||
name: node-24-built-tree
|
||
path: ${{ runner.temp }}/node-24-built-tree.tar.gz
|
||
if-no-files-found: error
|
||
retention-days: 1
|
||
compression-level: 0
|
||
|
||
node-24-coverage:
|
||
if: github.event_name == 'pull_request'
|
||
runs-on: >-
|
||
${{ vars.DSH_CI_FAILOVER == 'selfhosted'
|
||
&& github.event.pull_request.user.login != 'dependabot[bot]'
|
||
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|
||
|| 'dsh-enterprise-ubuntu-24-04-32core-test' }}
|
||
name: node 24 / coverage
|
||
env:
|
||
# Failover shrinks the worker bound: the hosted 32-core runner is
|
||
# exclusive to one job, but the failover pool shares one 64-core VM
|
||
# across six always-on runner instances, and the timing-sensitive
|
||
# process suites have documented aggregate-contention failures.
|
||
# 8 × 6 instances = 48 workers worst case on 64 cores.
|
||
DSH_COVERAGE_MAX_WORKERS: ${{ vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '24' }}
|
||
DSH_GATE_CONCURRENCY: '8'
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||
|
||
- name: Configure pnpm store path
|
||
id: pnpm-store
|
||
run: |
|
||
store_root="$HOME/.local/share/pnpm/store"
|
||
echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
|
||
store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
|
||
echo "path=$store_path" >> "$GITHUB_OUTPUT"
|
||
|
||
# Skipped under failover: the self-hosted VM's persistent pnpm store
|
||
# already serves warm installs, while restoring the hosted archive
|
||
# would spend ~52 s pulling ~180 MB into that populated store.
|
||
- uses: actions/cache/restore@v4
|
||
if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
|
||
with:
|
||
path: ${{ steps.pnpm-store.outputs.path }}
|
||
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
|
||
|
||
- name: Install dependencies and prepare bubblewrap
|
||
run: |
|
||
pnpm install --frozen-lockfile &
|
||
install_pid=$!
|
||
bash scripts/prepare-ci-bubblewrap.sh &
|
||
sandbox_pid=$!
|
||
install_status=0
|
||
wait "$install_pid" || install_status=$?
|
||
sandbox_status=0
|
||
wait "$sandbox_pid" || sandbox_status=$?
|
||
if (( install_status != 0 )); then exit "$install_status"; fi
|
||
exit "$sandbox_status"
|
||
|
||
- name: Run exhaustive coverage
|
||
run: pnpm run check:ci:coverage
|
||
|
||
node-24-consumers:
|
||
needs: node-24
|
||
if: github.event_name == 'pull_request'
|
||
runs-on: >-
|
||
${{ vars.DSH_CI_FAILOVER == 'selfhosted'
|
||
&& github.event.pull_request.user.login != 'dependabot[bot]'
|
||
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|
||
|| 'dsh-enterprise-ubuntu-latest-32core-test' }}
|
||
name: node 24 / snapshots and artifacts
|
||
env:
|
||
DSH_ESLINT_CACHE: '1'
|
||
DSH_ESLINT_CONCURRENCY: '8'
|
||
DSH_GATE_CONCURRENCY: '8'
|
||
DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
|
||
DSH_PUBLINT_CONCURRENCY: '8'
|
||
# Failover halves snapshot concurrency for the shared 64-core VM.
|
||
DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '12' || '32' }}
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- uses: actions/download-artifact@v8
|
||
with:
|
||
name: node-24-built-tree
|
||
path: ${{ runner.temp }}
|
||
|
||
- name: Restore built tree
|
||
run: tar -xzf "$RUNNER_TEMP/node-24-built-tree.tar.gz"
|
||
|
||
- uses: actions/cache/restore@v4
|
||
with:
|
||
path: .cache/eslint
|
||
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||
|
||
- name: Configure pnpm store path
|
||
id: pnpm-store
|
||
run: |
|
||
store_root="$HOME/.local/share/pnpm/store"
|
||
echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
|
||
store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
|
||
echo "path=$store_path" >> "$GITHUB_OUTPUT"
|
||
|
||
# Skipped under failover — see the coverage lane's identical rationale.
|
||
- uses: actions/cache/restore@v4
|
||
if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
|
||
with:
|
||
path: ${{ steps.pnpm-store.outputs.path }}
|
||
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
|
||
|
||
- name: Install dependencies and prepare bubblewrap
|
||
run: |
|
||
pnpm install --frozen-lockfile &
|
||
install_pid=$!
|
||
bash scripts/prepare-ci-bubblewrap.sh &
|
||
sandbox_pid=$!
|
||
install_status=0
|
||
wait "$install_pid" || install_status=$?
|
||
sandbox_status=0
|
||
wait "$sandbox_pid" || sandbox_status=$?
|
||
if (( install_status != 0 )); then exit "$install_status"; fi
|
||
exit "$sandbox_status"
|
||
|
||
- name: Run compatibility, snapshot, and artifact gates
|
||
run: |
|
||
pnpm run check:ci:lint &
|
||
lint_pid=$!
|
||
pnpm run check:node-compat &
|
||
compat_pid=$!
|
||
DSH_EXAMPLE_MODE=lib pnpm run test:snapshot &
|
||
snapshot_pid=$!
|
||
pnpm run publint &
|
||
publint_pid=$!
|
||
pnpm run verify-node-next-types &
|
||
node_next_pid=$!
|
||
pnpm run verify-built-package-invariants &
|
||
built_invariants_pid=$!
|
||
DSH_EXAMPLE_MODE=lib pnpm exec vitest run --config vitest.e2e.config.ts \
|
||
examples/headless-agent/tests/keyless-smoke.e2e.ts \
|
||
examples/tui-agent/tests/tui-keyless-smoke.e2e.ts \
|
||
packages/examples/cli-demo/tests/built-bin.e2e.ts \
|
||
packages/examples/acp-demo/tests/built-bin.e2e.ts \
|
||
packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts \
|
||
packages/workflow/workflow-workerthread/tests/built-worker.e2e.ts \
|
||
packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts &
|
||
built_bin_pid=$!
|
||
|
||
final_status=0
|
||
capture_status() {
|
||
local child_status=0
|
||
wait "$1" || child_status=$?
|
||
if (( final_status == 0 && child_status != 0 )); then
|
||
final_status=$child_status
|
||
fi
|
||
}
|
||
for child_pid in \
|
||
"$lint_pid" "$compat_pid" "$snapshot_pid" \
|
||
"$publint_pid" "$node_next_pid" "$built_invariants_pid" "$built_bin_pid"
|
||
do
|
||
capture_status "$child_pid"
|
||
done
|
||
exit "$final_status"
|
||
|
||
|
||
node-compat:
|
||
if: github.event_name == 'pull_request'
|
||
# Each compatibility contract receives an independent standard hosted job.
|
||
runs-on: ${{ matrix.runner }}
|
||
name: ${{ matrix.name }}
|
||
env:
|
||
DSH_GATE_CONCURRENCY: ${{ matrix.gate_concurrency }}
|
||
DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- node: '22.19'
|
||
name: node 22.19
|
||
runner: ubuntu-latest
|
||
gate_concurrency: '1'
|
||
- node: 26
|
||
name: node 26
|
||
runner: ubuntu-latest
|
||
gate_concurrency: '1'
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version: ${{ matrix.node }}
|
||
cache: pnpm
|
||
|
||
- name: Install (immutable)
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Run compatibility smokes
|
||
run: pnpm run check:node-compat
|
||
|
||
python-sdk:
|
||
if: github.event_name == 'pull_request'
|
||
runs-on: ubuntu-latest
|
||
name: python 3.10 / keyless SDK
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
|
||
- uses: actions/setup-python@v6
|
||
with:
|
||
python-version: '3.10'
|
||
cache: pip
|
||
|
||
- name: Install uv
|
||
run: python -m pip install uv==0.11.23
|
||
|
||
- name: Run complete keyless Python suite
|
||
run: uv run --python 3.10 --group test --project python/sdk pytest
|
||
|
||
# The required pull-request Windows signal: the two blocking win32 surfaces
|
||
# (workspace build, production site) execute with real, checksum-verified
|
||
# Windows Node under Wine on standard hosted Linux. The master
|
||
# serial-windows job below keeps the complete native-kernel inventory —
|
||
# including the observational portability gates this lane does not run —
|
||
# on real windows-2025. Direct tool entrypoints stand in for pnpm's cmd
|
||
# shims, which a Linux-side install does not create; layout, fidelity
|
||
# limits, and measured timings live in
|
||
# .agents/notes/implemented/process/2026-07-27-wine-windows-gates-experiment.md
|
||
windows:
|
||
if: github.event_name == 'pull_request'
|
||
runs-on: ubuntu-latest
|
||
name: windows node 24 / wine blocking
|
||
timeout-minutes: 15
|
||
env:
|
||
WINEDEBUG: '-all'
|
||
WINEARCH: win64
|
||
# Skip Wine Mono / Gecko installers: Node needs neither.
|
||
WINEDLLOVERRIDES: 'mscoree,mshtml='
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||
|
||
- name: Configure pnpm store path
|
||
id: pnpm-store
|
||
run: |
|
||
store_root="$HOME/.local/share/pnpm/store"
|
||
echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
|
||
store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
|
||
echo "path=$store_path" >> "$GITHUB_OUTPUT"
|
||
|
||
- uses: actions/cache/restore@v4
|
||
with:
|
||
path: ${{ steps.pnpm-store.outputs.path }}
|
||
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
|
||
|
||
# Master's wine-apt-cache job seeds the default-branch scope every pull
|
||
# request can read; a save from this job only reaches reruns of the
|
||
# same merge ref.
|
||
- name: Compose Wine apt cache key
|
||
id: wine-cache-key
|
||
run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT"
|
||
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: ~/wine-debs
|
||
key: ${{ steps.wine-cache-key.outputs.key }}
|
||
|
||
- name: Install dependencies and provision Wine concurrently
|
||
run: |
|
||
# Windows-lane install-time overrides. supportedArchitectures
|
||
# additionally materializes the win32-x64 platform packages
|
||
# (@esbuild/win32-x64, rolldown and rollup MSVC bindings) the
|
||
# Windows toolchain resolves at runtime; nodeLinker: hoisted lays
|
||
# node_modules out flat with real files because Windows Node under
|
||
# Wine does not realpath pnpm's isolated-layout symlinks. Neither
|
||
# override is recorded in the lockfile, so --frozen-lockfile stays
|
||
# valid. --ignore-scripts skips Linux lifecycle scripts no gate in
|
||
# this lane loads; the win32 binaries ship prebuilt.
|
||
cat >> pnpm-workspace.yaml <<'EOF'
|
||
|
||
nodeLinker: hoisted
|
||
supportedArchitectures:
|
||
os: [current, win32]
|
||
cpu: [current, x64]
|
||
EOF
|
||
|
||
pnpm install --frozen-lockfile --ignore-scripts &
|
||
install_pid=$!
|
||
|
||
provision_wine() {
|
||
set -euo pipefail
|
||
# Wine from the apt cache when present; else download the full
|
||
# dependency closure once and keep it for the next run. The
|
||
# `wine` dispatcher package (not bare `wine64`) is what puts a
|
||
# binary on PATH.
|
||
if compgen -G "$HOME/wine-debs/*.deb" > /dev/null; then
|
||
sudo apt-get install -y --no-install-recommends "$HOME"/wine-debs/*.deb
|
||
else
|
||
sudo apt-get update
|
||
sudo apt-get install -y --no-install-recommends --download-only wine
|
||
mkdir -p "$HOME/wine-debs"
|
||
cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/" 2>/dev/null || true
|
||
sudo apt-get install -y --no-install-recommends wine
|
||
fi
|
||
WINE_BIN=''
|
||
for candidate in "$(command -v wine || true)" "$(command -v wine64 || true)" /usr/lib/wine/wine64; do
|
||
if [ -n "$candidate" ] && [ -x "$candidate" ]; then WINE_BIN="$candidate"; break; fi
|
||
done
|
||
[ -n "$WINE_BIN" ] || { echo '::error::no wine binary found after install'; exit 1; }
|
||
echo "WINE_BIN=$WINE_BIN" >> "$GITHUB_ENV"
|
||
|
||
# Windows Node for the repo's primary line, checksum-verified
|
||
# against the same dist directory.
|
||
version=$(curl -fsSL https://nodejs.org/dist/index.json \
|
||
| jq -r --arg p "v${PRIMARY_NODE_VERSION}." '[.[] | select(.version | startswith($p))][0].version')
|
||
echo "Windows Node: $version"
|
||
curl -fsSL -o "$RUNNER_TEMP/node-win.zip" \
|
||
"https://nodejs.org/dist/${version}/node-${version}-win-x64.zip"
|
||
curl -fsSL "https://nodejs.org/dist/${version}/SHASUMS256.txt" \
|
||
| awk -v a="node-${version}-win-x64.zip" '$2 == a { print $1 " '"$RUNNER_TEMP"'/node-win.zip" }' \
|
||
| sha256sum --check -
|
||
unzip -q "$RUNNER_TEMP/node-win.zip" -d "$RUNNER_TEMP/node-win"
|
||
echo "NODE_WIN=$RUNNER_TEMP/node-win/node-${version}-win-x64/node.exe" >> "$GITHUB_ENV"
|
||
|
||
"$WINE_BIN" wineboot --init || true
|
||
wineserver -w || true
|
||
}
|
||
provision_wine &
|
||
wine_pid=$!
|
||
|
||
install_status=0
|
||
wait "$install_pid" || install_status=$?
|
||
wine_status=0
|
||
wait "$wine_pid" || wine_status=$?
|
||
if (( install_status != 0 )); then exit "$install_status"; fi
|
||
exit "$wine_status"
|
||
|
||
- name: Resolve entrypoints, link vue, smoke Windows Node
|
||
run: |
|
||
# Node under Wine cannot attach stdio to the Actions runner's pipes
|
||
# (Socket open EBADF at bootstrap), so every invocation runs through
|
||
# this wrapper: stdio to a regular file, replayed after exit.
|
||
cat > "$RUNNER_TEMP/wine-node.sh" <<'SH'
|
||
#!/usr/bin/env bash
|
||
set -u
|
||
log="$1"; shift
|
||
"$WINE_BIN" "$NODE_WIN" "$@" < /dev/null > "$log" 2>&1
|
||
status=$?
|
||
tail -n 300 "$log"
|
||
exit "$status"
|
||
SH
|
||
chmod +x "$RUNNER_TEMP/wine-node.sh"
|
||
|
||
resolve() {
|
||
local name="$1"; shift
|
||
for p in "$@"; do
|
||
if [ -f "$p" ]; then echo "$name=$PWD/$p" >> "$GITHUB_ENV"; return 0; fi
|
||
done
|
||
echo "::error::$name not found at any of: $*"; return 1
|
||
}
|
||
resolve TSC_JS node_modules/typescript/bin/tsc
|
||
resolve TSDOWN_JS node_modules/tsdown/dist/run.mjs
|
||
resolve VITEPRESS_JS website/node_modules/vitepress/bin/vitepress.js node_modules/vitepress/bin/vitepress.js
|
||
|
||
# VitePress links vue into the site's node_modules at build time;
|
||
# Wine cannot CREATE Windows symlinks (ENOTSUP) but follows
|
||
# pre-existing Unix ones, so lay the link down host-side.
|
||
if [ -d node_modules/vue ] && [ ! -e website/node_modules/vue ]; then
|
||
mkdir -p website/node_modules
|
||
ln -s ../../node_modules/vue website/node_modules/vue
|
||
fi
|
||
|
||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/smoke.log" -p "'smoke: ' + process.platform + ' ' + process.arch + ' ' + process.version"
|
||
|
||
# The two blocking surfaces run concurrently, the same shape run-gates
|
||
# gives ci-windows-blocking on native Windows: `build` = tsc -b then
|
||
# tsdown, `production site` = the VitePress build. Both statuses are
|
||
# captured so one failure cannot hide the other's result.
|
||
- name: Run blocking Windows gates concurrently under Wine
|
||
run: |
|
||
build_gate() {
|
||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/tsc.log" "$TSC_JS" -b --pretty false || return $?
|
||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/tsdown.log" "$TSDOWN_JS"
|
||
}
|
||
site_gate() {
|
||
cd website
|
||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/site.log" "$VITEPRESS_JS" build .
|
||
}
|
||
start=$SECONDS
|
||
build_gate > "$RUNNER_TEMP/build-gate.out" 2>&1 &
|
||
build_pid=$!
|
||
site_gate > "$RUNNER_TEMP/site-gate.out" 2>&1 &
|
||
site_pid=$!
|
||
build_status=0
|
||
wait "$build_pid" || build_status=$?
|
||
site_status=0
|
||
wait "$site_pid" || site_status=$?
|
||
echo "== build gate (exit $build_status, $((SECONDS - start))s elapsed) =="
|
||
tail -n 120 "$RUNNER_TEMP/build-gate.out"
|
||
echo "== production site gate (exit $site_status, $((SECONDS - start))s elapsed) =="
|
||
tail -n 120 "$RUNNER_TEMP/site-gate.out"
|
||
if (( build_status != 0 )); then exit "$build_status"; fi
|
||
exit "$site_status"
|
||
|
||
- name: Shut down wineserver
|
||
if: always()
|
||
run: wineserver -k 2>/dev/null || true
|
||
|
||
# Master seeds the Wine apt-archive cache in the default-branch scope,
|
||
# which every pull request's windows job can restore; saves from
|
||
# pull-request runs are scoped to their own merge ref and help nobody
|
||
# else. Runs in seconds when the image version already has a cache.
|
||
wine-apt-cache:
|
||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||
name: wine apt cache
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
steps:
|
||
- name: Compose Wine apt cache key
|
||
id: wine-cache-key
|
||
run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT"
|
||
|
||
- uses: actions/cache@v4
|
||
id: wine-cache
|
||
with:
|
||
path: ~/wine-debs
|
||
key: ${{ steps.wine-cache-key.outputs.key }}
|
||
|
||
- name: Download the Wine dependency closure
|
||
if: steps.wine-cache.outputs.cache-hit != 'true'
|
||
run: |
|
||
sudo apt-get update
|
||
sudo apt-get install -y --no-install-recommends --download-only wine
|
||
mkdir -p "$HOME/wine-debs"
|
||
cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/"
|
||
du -sh "$HOME/wine-debs"
|
||
|
||
# Master pushes run only the serial reference jobs below.
|
||
# Each host executes the complete, unsharded primary Node aggregate with one
|
||
# gate worker, giving reviewers a simple cross-platform oracle for completeness
|
||
# and timing.
|
||
serial-linux:
|
||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||
name: serial / linux
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
fetch-depth: 2
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||
|
||
- name: Configure pnpm store path
|
||
id: pnpm-store
|
||
run: |
|
||
store_root="$HOME/.local/share/pnpm/store"
|
||
echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
|
||
store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
|
||
echo "path=$store_path" >> "$GITHUB_OUTPUT"
|
||
|
||
# Master refreshes the caches that pull requests restore without saving.
|
||
# The store cache stays a hand-rolled actions/cache step rather than
|
||
# setup-node's `cache: pnpm`: the enterprise pull-request jobs above
|
||
# restore exactly this key and path, and setup-node's built-in cache
|
||
# uses its own key format — converting this producer would silently
|
||
# starve their documented restore-only optimization.
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: ${{ steps.pnpm-store.outputs.path }}
|
||
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
|
||
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: .cache/eslint
|
||
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-
|
||
|
||
- name: Install (immutable)
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Prepare bubblewrap (unrestrict userns)
|
||
run: bash scripts/prepare-ci-bubblewrap.sh
|
||
|
||
- name: Run complete unsharded primary Node CI serially
|
||
env:
|
||
DSH_ARCHIVE_BASE_REF: ${{ github.event.before }}
|
||
DSH_COVERAGE_MAX_WORKERS: '1'
|
||
DSH_E2E_MAX_WORKERS: '1'
|
||
DSH_ESLINT_CACHE: '1'
|
||
DSH_GATE_CONCURRENCY: '1'
|
||
DSH_PUBLINT_CONCURRENCY: '1'
|
||
DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
|
||
run: pnpm run check:ci
|
||
|
||
# Hot-standby drill for the in-house self-hosted pool: every master move
|
||
# re-runs the complete unsharded aggregate on the persistent 64-core VM,
|
||
# continuously proving that environment can take over a required lane if
|
||
# the hosted pools degrade (the switch is then setting the writer-manageable
|
||
# DSH_CI_FAILOVER variable — see the failover runbook, no merge required).
|
||
# Push-triggered, so this lane always executes the base branch's own
|
||
# workflow definition. (Under failover, pull_request jobs do reach these
|
||
# runners with the PR merge ref's workflow — the boundary there is
|
||
# repository membership: private, forking disabled, Dependabot excluded.)
|
||
# Non-blocking for
|
||
# pull requests; no cache steps because the VM's persistent pnpm store and
|
||
# tool caches make them redundant (and saving here would poison the hosted
|
||
# cache namespace with self-hosted paths).
|
||
serial-linux-selfhosted:
|
||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||
name: serial / linux (self-hosted standby)
|
||
runs-on: [self-hosted, linux, x64, vm-backup]
|
||
steps:
|
||
# Full history + DSH_ARCHIVE_BASE_REF below: same frozen-archive
|
||
# comparison as serial-linux. Depth 2 would miss github.event.before
|
||
# on multi-commit or force pushes; full fetch is cheap here because
|
||
# checkout resolves against the VM's local mirror.
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||
|
||
- name: Configure persistent pnpm store
|
||
run: echo "PNPM_CONFIG_STORE_DIR=$HOME/.local/share/pnpm/store" >> "$GITHUB_ENV"
|
||
|
||
- name: Install (immutable)
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Prepare bubblewrap (unrestrict userns)
|
||
run: bash scripts/prepare-ci-bubblewrap.sh
|
||
|
||
- name: Run complete unsharded primary Node CI serially
|
||
env:
|
||
DSH_ARCHIVE_BASE_REF: ${{ github.event.before }}
|
||
DSH_COVERAGE_MAX_WORKERS: '1'
|
||
DSH_E2E_MAX_WORKERS: '1'
|
||
DSH_ESLINT_CACHE: '1'
|
||
DSH_GATE_CONCURRENCY: '1'
|
||
DSH_PUBLINT_CONCURRENCY: '1'
|
||
DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
|
||
run: pnpm run check:ci
|
||
|
||
serial-macos:
|
||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||
name: serial / macos
|
||
runs-on: macos-latest
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||
|
||
- name: Install (immutable)
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Run complete unsharded primary Node CI serially
|
||
env:
|
||
DSH_COVERAGE_MAX_WORKERS: '1'
|
||
DSH_E2E_MAX_WORKERS: '1'
|
||
DSH_GATE_CONCURRENCY: '1'
|
||
DSH_PUBLINT_CONCURRENCY: '1'
|
||
DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
|
||
run: pnpm run check:ci
|
||
|
||
serial-windows:
|
||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||
name: serial / windows
|
||
runs-on: windows-2025
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
|
||
- name: Enable Developer Mode (symlink support)
|
||
shell: pwsh
|
||
run: >-
|
||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
|
||
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||
|
||
# Master refreshes the small cache that pull requests restore without
|
||
# putting package-store extraction back on the Windows critical path.
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: .cache/eslint
|
||
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-
|
||
|
||
- name: Install (immutable)
|
||
shell: pwsh
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Run complete unsharded primary Node CI serially
|
||
shell: pwsh
|
||
env:
|
||
DSH_COVERAGE_MAX_WORKERS: '1'
|
||
DSH_E2E_MAX_WORKERS: '1'
|
||
DSH_ESLINT_CACHE: '1'
|
||
DSH_GATE_CONCURRENCY: '1'
|
||
DSH_PUBLINT_CONCURRENCY: '1'
|
||
DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
|
||
run: pnpm run check:ci
|
||
|
||
# Manual, bounded comparison of the actual critical Linux and Windows lanes.
|
||
# The named pools are restricted at the organization level to this repository.
|
||
larger-runner-benchmark:
|
||
if: github.event_name == 'workflow_dispatch' && inputs.suite == 'larger-runner-benchmark'
|
||
runs-on: ${{ matrix.runner }}
|
||
timeout-minutes: 15
|
||
strategy:
|
||
fail-fast: false
|
||
max-parallel: 12
|
||
matrix:
|
||
include:
|
||
- platform: linux
|
||
cores: '4'
|
||
runner: dsh-ubuntu-24-04-4core
|
||
workload: typecheck
|
||
- platform: linux
|
||
cores: '8'
|
||
runner: dsh-ubuntu-24-04-8core
|
||
workload: typecheck
|
||
- platform: linux
|
||
cores: '16'
|
||
runner: dsh-ubuntu-24-04-16core
|
||
workload: typecheck
|
||
- platform: linux
|
||
cores: '32'
|
||
runner: dsh-ubuntu-24-04-32core
|
||
workload: typecheck
|
||
- platform: linux
|
||
cores: '64'
|
||
runner: dsh-ubuntu-24-04-64core
|
||
workload: typecheck
|
||
- platform: linux
|
||
cores: '96'
|
||
runner: dsh-ubuntu-24-04-96core
|
||
workload: typecheck
|
||
- platform: windows
|
||
cores: '4'
|
||
runner: dsh-windows-2025-4core
|
||
workload: production-site
|
||
- platform: windows
|
||
cores: '8'
|
||
runner: dsh-windows-2025-8core
|
||
workload: production-site
|
||
- platform: windows
|
||
cores: '16'
|
||
runner: dsh-windows-2025-16core
|
||
workload: production-site
|
||
- platform: windows
|
||
cores: '32'
|
||
runner: dsh-windows-2025-32core
|
||
workload: production-site
|
||
- platform: windows
|
||
cores: '64'
|
||
runner: dsh-windows-2025-64core
|
||
workload: production-site
|
||
- platform: windows
|
||
cores: '96'
|
||
runner: dsh-windows-2025-96core
|
||
workload: production-site
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
|
||
# The Windows lanes deliberately skip the store cache like the required
|
||
# windows job; an empty cache input disables setup-node's caching.
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||
cache: ${{ matrix.platform == 'linux' && 'pnpm' || '' }}
|
||
|
||
- name: Report runner capacity
|
||
run: >-
|
||
node -e "const os = require('node:os');
|
||
console.log(JSON.stringify({ arch: process.arch, cpus: os.cpus().length,
|
||
memoryGiB: Math.round(os.totalmem() / 2 ** 30) }))"
|
||
|
||
- name: Install (immutable)
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Run critical Linux typecheck lane
|
||
if: matrix.platform == 'linux'
|
||
run: pnpm run typecheck
|
||
|
||
- name: Run critical Windows production-site lane
|
||
if: matrix.platform == 'windows'
|
||
run: pnpm run docs:build
|
||
|
||
# Manual comparison of the intended low-fanout topology. Linux runs the
|
||
# complete unsharded primary aggregate with bounded in-runner parallelism;
|
||
# Windows runs both blocking build surfaces concurrently through run-gates.
|
||
consolidated-runner-benchmark:
|
||
if: github.event_name == 'workflow_dispatch' && inputs.suite == 'consolidated-runner-benchmark'
|
||
runs-on: ${{ matrix.runner }}
|
||
timeout-minutes: 15
|
||
strategy:
|
||
fail-fast: false
|
||
max-parallel: 12
|
||
matrix:
|
||
include:
|
||
- platform: linux
|
||
cores: '4'
|
||
runner: dsh-ubuntu-24-04-4core
|
||
workers: '4'
|
||
- platform: linux
|
||
cores: '8'
|
||
runner: dsh-ubuntu-24-04-8core
|
||
workers: '8'
|
||
- platform: linux
|
||
cores: '16'
|
||
runner: dsh-ubuntu-24-04-16core
|
||
workers: '16'
|
||
- platform: linux
|
||
cores: '32'
|
||
runner: dsh-ubuntu-24-04-32core
|
||
workers: '32'
|
||
- platform: linux
|
||
cores: '64'
|
||
runner: dsh-ubuntu-24-04-64core
|
||
workers: '32'
|
||
- platform: linux
|
||
cores: '96'
|
||
runner: dsh-ubuntu-24-04-96core
|
||
workers: '32'
|
||
- platform: windows
|
||
cores: '4'
|
||
runner: dsh-windows-2025-4core
|
||
workers: '2'
|
||
- platform: windows
|
||
cores: '8'
|
||
runner: dsh-windows-2025-8core
|
||
workers: '2'
|
||
- platform: windows
|
||
cores: '16'
|
||
runner: dsh-windows-2025-16core
|
||
workers: '2'
|
||
- platform: windows
|
||
cores: '32'
|
||
runner: dsh-windows-2025-32core
|
||
workers: '2'
|
||
- platform: windows
|
||
cores: '64'
|
||
runner: dsh-windows-2025-64core
|
||
workers: '2'
|
||
- platform: windows
|
||
cores: '96'
|
||
runner: dsh-windows-2025-96core
|
||
workers: '2'
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
|
||
# Unlike the larger-runner suite, both platforms cache the store here:
|
||
# the consolidated topology measures cache mechanics as workload.
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||
cache: pnpm
|
||
|
||
- name: Report runner capacity
|
||
run: >-
|
||
node -e "const os = require('node:os');
|
||
console.log(JSON.stringify({ arch: process.arch, cpus: os.cpus().length,
|
||
memoryGiB: Math.round(os.totalmem() / 2 ** 30) }))"
|
||
|
||
- uses: actions/cache@v4
|
||
if: matrix.platform == 'linux'
|
||
with:
|
||
path: .cache/eslint
|
||
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-
|
||
|
||
- name: Install and prepare Linux
|
||
if: matrix.platform == 'linux'
|
||
run: |
|
||
pnpm install --frozen-lockfile &
|
||
install_pid=$!
|
||
bash scripts/prepare-ci-bubblewrap.sh &
|
||
sandbox_pid=$!
|
||
install_status=0
|
||
wait "$install_pid" || install_status=$?
|
||
sandbox_status=0
|
||
wait "$sandbox_pid" || sandbox_status=$?
|
||
if (( install_status != 0 )); then exit "$install_status"; fi
|
||
exit "$sandbox_status"
|
||
|
||
- name: Install (immutable)
|
||
if: matrix.platform == 'windows'
|
||
shell: pwsh
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Run complete unsharded primary Node CI concurrently
|
||
if: matrix.platform == 'linux'
|
||
env:
|
||
DSH_COVERAGE_MAX_WORKERS: ${{ matrix.workers }}
|
||
DSH_ESLINT_CACHE: '1'
|
||
DSH_ESLINT_CONCURRENCY: ${{ matrix.workers }}
|
||
DSH_GATE_CONCURRENCY: ${{ matrix.workers }}
|
||
DSH_PUBLINT_CONCURRENCY: ${{ matrix.workers }}
|
||
DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ matrix.workers }}
|
||
run: pnpm run check:ci
|
||
|
||
- name: Run blocking Windows builds concurrently
|
||
if: matrix.platform == 'windows'
|
||
shell: pwsh
|
||
env:
|
||
DSH_GATE_CONCURRENCY: ${{ matrix.workers }}
|
||
run: pnpm run check:ci:windows-blocking
|
||
|
||
# Single stable required check for branch protection: require "all checks
|
||
# passed" instead of enumerating matrix legs whose names change as lanes and
|
||
# node versions evolve. Every blocking job in THIS workflow must be listed in
|
||
# `needs`; observational Windows gates share the required Windows job but are
|
||
# marked non-blocking inside run-gates. (`needs` cannot reach across workflow
|
||
# files; e2e.yml stays its own check.)
|
||
# `if: always()` is load-bearing: without it a failed dependency
|
||
# would SKIP this job, and GitHub counts a skipped required check as passing
|
||
# — so this job always runs and fails on any non-success result, including
|
||
# 'cancelled' and 'skipped'.
|
||
all-checks-passed:
|
||
name: all checks passed
|
||
# This bookkeeping-only verdict must not depend on custom-pool
|
||
# provisioning (master's #734 moved it to ubuntu-latest) — and under
|
||
# failover it follows the same selector as the worker jobs it
|
||
# aggregates, so a standard-hosted outage cannot strand the
|
||
# branch-protection verdict either (observed 2026-07-27).
|
||
runs-on: >-
|
||
${{ vars.DSH_CI_FAILOVER == 'selfhosted'
|
||
&& github.event.pull_request.user.login != 'dependabot[bot]'
|
||
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|
||
|| 'ubuntu-latest' }}
|
||
needs: [node-24, node-24-coverage, node-24-consumers, node-compat, python-sdk, windows]
|
||
if: always() && github.event_name == 'pull_request'
|
||
steps:
|
||
- name: Fail if any needed job did not succeed
|
||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')
|
||
run: |
|
||
echo "::error::Needed job results: ${{ join(needs.*.result, ', ') }}"
|
||
exit 1
|
||
- name: All checks passed
|
||
run: echo "All needed jobs succeeded (${{ join(needs.*.result, ', ') }})"
|