mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
* ci: run native Windows PR jobs in a separate workflow GitHub re-runs failed jobs only after a whole run finishes, so slow Windows jobs blocked re-running failed Linux jobs. CI Windows now owns the native Windows jobs and the Windows x64 Python runtime target, with its own required 'windows checks passed' verdict. Refs #6893 * ci: run Python PR jobs in a separate workflow Python rarely fails while the Windows lanes run long, so the Python SDK suite and the Linux/Windows x64 runtime matrix move into CI Python. The three pull-request workflows can now be re-run without waiting for each other, and branch protection gains "python checks passed" beside the existing verdicts. Refs #6893 * docs: record the three PR verdicts in the failover runbook * test: pin the shared environment across the PR workflows * test: read the python SDK lane from CI Python * test(sdk): parse session JSONL frames in the keyless smoke Node 24.19 returned only the first frame of a concatenated zstd log, so the single JSON.parse saw just the header; 24.22 returns every frame and the JSONL body broke the parse. Read frames explicitly and select the main session by its header so both versions agree.
516 lines
24 KiB
YAML
516 lines
24 KiB
YAML
name: CI
|
|
|
|
on:
|
|
pull_request:
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
PRIMARY_NODE_VERSION: '24'
|
|
# CI runs must never report to the production telemetry endpoint baked
|
|
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
|
|
DSH_TELEMETRY_DISABLED: '1'
|
|
|
|
# Cancel a superseded pull-request run on a new push so a fresh head does not
|
|
# queue a second full run behind a stale one (paid enterprise runners
|
|
# would otherwise stack with no auto-cancellation).
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
|
|
# Three enterprise jobs isolate coverage, static analysis, and the
|
|
# build-backed consumer tail. The consumer job owns the only Linux build so
|
|
# all three jobs enter runner allocation independently.
|
|
#
|
|
# FAILOVER (Linux): each Linux enterprise job resolves its pool through the
|
|
# DSH_CI_FAILOVER_LINUX 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.
|
|
# Setting the variable to 'blacksmith' instead routes the same jobs onto
|
|
# Blacksmith's hosted runners sized per workload (see
|
|
# .agents/notes/implemented/process/2026-09-09-blacksmith-failover-leg.md). The
|
|
# in-house pool's readiness is re-proven on every master push by the
|
|
# serial-linux-selfhosted standby lane in ci-master.yml. Native Windows jobs
|
|
# and their separate DSH_CI_FAILOVER_WINDOWS switch live in ci-windows.yml.
|
|
node-24:
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: >-
|
|
${{ vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-8vcpu-ubuntu-2404'
|
|
|| vars.DSH_CI_FAILOVER_LINUX == 'selfhosted'
|
|
&& github.event.pull_request.user.login != 'dependabot[bot]'
|
|
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|
|
|| 'dsh-ubuntu-24-04-16core' }}
|
|
name: node 24 / static
|
|
env:
|
|
# Shared hosts expose host CPUs, not an isolated allocation.
|
|
DSH_GATE_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '' }}
|
|
# Stop the aggregate at the first blocking gate failure so a red run
|
|
# does not keep burning enterprise runner time on the remaining gates.
|
|
DSH_GATE_FAIL_FAST: '1'
|
|
steps:
|
|
# Runner cleanup owns tool caches and fixtures, including cancelled runs.
|
|
- name: Use runner-owned temporary storage
|
|
run: |
|
|
echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV"
|
|
|
|
# Fetch complete history so the archive gate can read the trusted PR base from a reused shallow checkout.
|
|
- uses: actions/checkout@v7.0.1
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
# Redirect the Node compile cache (enabled by pnpm and TypeScript) off
|
|
# the root partition's /tmp before the first pnpm call in this lane.
|
|
- name: Redirect Node compile cache to runner temp
|
|
run: echo "NODE_COMPILE_CACHE=${{ runner.temp }}/node-compile-cache" >> "$GITHUB_ENV"
|
|
|
|
- uses: pnpm/action-setup@v5.0.0
|
|
with:
|
|
dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }}
|
|
|
|
- uses: actions/setup-node@v7.0.0
|
|
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@v6.1.0
|
|
if: vars.DSH_CI_FAILOVER_LINUX != '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
|
|
|
|
node-24-coverage:
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: >-
|
|
${{ vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-16vcpu-ubuntu-2404'
|
|
|| vars.DSH_CI_FAILOVER_LINUX == 'selfhosted'
|
|
&& github.event.pull_request.user.login != 'dependabot[bot]'
|
|
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|
|
|| 'dsh-ubuntu-24-04-16core' }}
|
|
name: node 24 / coverage
|
|
env:
|
|
# Shared hosts expose host CPUs, not an isolated allocation.
|
|
DSH_COVERAGE_MAX_WORKERS: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '6' || '' }}
|
|
DSH_COVERAGE_PARTITIONS: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '4' || '' }}
|
|
DSH_GATE_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '3' || '' }}
|
|
# Managed-scope teardown cases run past the default per-test budget when
|
|
# this lane's partitions, workers, and sibling gates share one host: the
|
|
# disposal cases in subprocess-local and bash-sandbox exceeded 5000ms on
|
|
# the hosted image (run 34449848541) while the same commit stayed inside
|
|
# the budget on the in-house pool; the lane is green with this value
|
|
# (run 34457655892). The Windows coverage lane grants the same budget for
|
|
# the same reason.
|
|
DSH_COVERAGE_TEST_TIMEOUT_MS: '90000'
|
|
# A gate failure aborts the sibling gate instead of waiting out its
|
|
# multi-minute instrumented run.
|
|
DSH_GATE_FAIL_FAST: '1'
|
|
steps:
|
|
# Runner cleanup owns tool caches and fixtures, including cancelled runs.
|
|
- name: Use runner-owned temporary storage
|
|
run: |
|
|
echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV"
|
|
|
|
- uses: actions/checkout@v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Restore coverage duration history
|
|
uses: actions/cache/restore@v6.1.0
|
|
with:
|
|
path: .coverage-times.json
|
|
key: coverage-times-Linux-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_LINUX || 'enterprise' }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
restore-keys: |
|
|
coverage-times-Linux-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_LINUX || 'enterprise' }}-
|
|
|
|
# Redirect the Node compile cache (enabled by pnpm and TypeScript) off
|
|
# the root partition's /tmp before the first pnpm call in this lane.
|
|
- name: Redirect Node compile cache to runner temp
|
|
run: echo "NODE_COMPILE_CACHE=${{ runner.temp }}/node-compile-cache" >> "$GITHUB_ENV"
|
|
|
|
- uses: pnpm/action-setup@v5.0.0
|
|
with:
|
|
dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }}
|
|
|
|
- uses: actions/setup-node@v7.0.0
|
|
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"
|
|
|
|
# Self-hosted runners reuse the shared persistent pnpm store.
|
|
# Hosted runners restore the remote cache into their ephemeral store.
|
|
- uses: actions/cache/restore@v6.1.0
|
|
if: vars.DSH_CI_FAILOVER_LINUX != '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
|
|
|
|
- name: Save coverage duration history
|
|
if: ${{ !cancelled() }}
|
|
uses: actions/cache/save@v6.1.0
|
|
with:
|
|
path: .coverage-times.json
|
|
key: coverage-times-Linux-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_LINUX || 'enterprise' }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
|
|
node-24-bench:
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: ubuntu-24.04
|
|
name: node 24 / benchmarks
|
|
# Wall-clock budgets need an otherwise idle runner, so this job runs the
|
|
# benchmark lane alone instead of joining a concurrent gate aggregate.
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: pnpm/action-setup@v5.0.0
|
|
with:
|
|
dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }}
|
|
|
|
- uses: actions/setup-node@v7.0.0
|
|
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@v6.1.0
|
|
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: Install benchmark browser and hosted dependencies
|
|
run: pnpm --filter @deepseek-ai/dsh-benchmarks exec playwright install --with-deps chromium
|
|
|
|
- name: Run performance benchmarks
|
|
env:
|
|
DSH_GATE_VERBOSE: '1'
|
|
run: pnpm run check:ci:bench
|
|
|
|
# The report global teardown renders benchmarks/.dsh-report after the run,
|
|
# including after a failing budget assertion. A run that fails before
|
|
# recording any timing has no report, so missing files warn instead of
|
|
# failing the benchmark job a second time.
|
|
- name: Upload scaled benchmark report
|
|
if: ${{ !cancelled() }}
|
|
uses: actions/upload-artifact@v7.0.1
|
|
with:
|
|
name: benchmark-report
|
|
path: benchmarks/.dsh-report/
|
|
include-hidden-files: true
|
|
if-no-files-found: warn
|
|
retention-days: 7
|
|
|
|
node-24-consumers:
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: >-
|
|
${{ vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-16vcpu-ubuntu-2404'
|
|
|| vars.DSH_CI_FAILOVER_LINUX == 'selfhosted'
|
|
&& github.event.pull_request.user.login != 'dependabot[bot]'
|
|
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|
|
|| 'dsh-ubuntu-24-04-16core' }}
|
|
name: node 24 / snapshots and artifacts
|
|
env:
|
|
# Shared hosts expose host CPUs, not an isolated allocation.
|
|
DSH_GATE_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '10' || '' }}
|
|
DSH_OXLINT_THREADS: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '' }}
|
|
DSH_PUBLINT_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '' }}
|
|
DSH_WEB_SNAPSHOT_WORKERS: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '6' || '' }}
|
|
DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '12' || '' }}
|
|
DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
|
|
# A failing gate aborts its running siblings: a failing build stops the
|
|
# independent Node compatibility smoke, and a failing reader (e.g.
|
|
# publint) stops the remaining artifact consumers.
|
|
DSH_GATE_FAIL_FAST: '1'
|
|
steps:
|
|
# Runner cleanup owns tool caches and fixtures, including cancelled runs.
|
|
- name: Use runner-owned temporary storage
|
|
run: |
|
|
echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV"
|
|
echo "PLAYWRIGHT_BROWSERS_PATH=${RUNNER_TEMP%/*}/ms-playwright" >> "$GITHUB_ENV"
|
|
|
|
- uses: actions/checkout@v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# Redirect the Node compile cache (enabled by pnpm and TypeScript) off
|
|
# the root partition's /tmp before the first pnpm call in this lane.
|
|
- name: Redirect Node compile cache to runner temp
|
|
run: echo "NODE_COMPILE_CACHE=${{ runner.temp }}/node-compile-cache" >> "$GITHUB_ENV"
|
|
|
|
- uses: pnpm/action-setup@v5.0.0
|
|
with:
|
|
dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }}
|
|
|
|
- uses: actions/setup-node@v7.0.0
|
|
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 restore the pnpm store and Playwright caches without paying
|
|
# compression and upload on the required path. No master job saves these
|
|
# hosted cache keys, so each restore-keys fallback hits the matching archived
|
|
# entry until it evicts, after which the store is cold. Self-hosted runners
|
|
# reuse the shared persistent store instead of restoring the remote archive.
|
|
- uses: actions/cache/restore@v6.1.0
|
|
if: vars.DSH_CI_FAILOVER_LINUX != '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-
|
|
|
|
# Self-hosted runners retain their own browser cache between jobs.
|
|
- uses: actions/cache/restore@v6.1.0
|
|
if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
|
|
with:
|
|
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
|
|
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-playwright-
|
|
|
|
- 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"
|
|
|
|
# Blacksmith's Ubuntu HTTP archive endpoints can be unreachable while HTTPS works.
|
|
- name: Use HTTPS for Ubuntu archives on Blacksmith
|
|
if: vars.DSH_CI_FAILOVER_LINUX == 'blacksmith'
|
|
run: |
|
|
sudo python3 - <<'PY'
|
|
from pathlib import Path
|
|
import re
|
|
|
|
apt = Path('/etc/apt')
|
|
sources = [apt / 'sources.list', apt / 'blacksmith-ubuntu-mirrors.txt',
|
|
*(apt / 'sources.list.d').glob('*.list'),
|
|
*(apt / 'sources.list.d').glob('*.sources')]
|
|
for source in sources:
|
|
if source.is_file():
|
|
source.write_text(re.sub(
|
|
r'http://((?:[a-z]{2}\.)?archive\.ubuntu\.com|security\.ubuntu\.com)/',
|
|
r'https://\1/', source.read_text()))
|
|
PY
|
|
|
|
- name: Install Playwright Chromium, WebKit, and hosted dependencies
|
|
if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
|
|
# Ephemeral runner images can carry Ubuntu mirrors whose HTTP endpoints are unreachable.
|
|
run: |
|
|
sudo find /etc/apt -maxdepth 2 -type f \( -name '*.list' -o -name '*.sources' \) -exec sed -i -E 's@http://([a-z]{2}\.)?archive\.ubuntu\.com/ubuntu@https://archive.ubuntu.com/ubuntu@g; s@http://security\.ubuntu\.com/ubuntu@https://security.ubuntu.com/ubuntu@g' {} +
|
|
pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps chromium webkit
|
|
|
|
# The persistent VM image owns Playwright's Linux system packages; do
|
|
# not mutate the shared host with apt on every failover run.
|
|
- name: Install Playwright Chromium and WebKit on the failover VM
|
|
if: vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]'
|
|
run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium webkit
|
|
|
|
- name: Build and execute the Linux Office runtime
|
|
run: CI=true pnpm run prepare:primary-runtime --target linux-x64 --output "$RUNNER_TEMP/office-runtime" --cache "$RUNNER_TEMP/office-runtime-downloads"
|
|
|
|
- name: Run compatibility, snapshot, and artifact gates
|
|
run: pnpm run check:ci:consumers
|
|
|
|
|
|
node-compat:
|
|
if: github.event_name == 'pull_request'
|
|
# Under the selfhosted leg this job admits only repository-owned PR code
|
|
# to the persistent shared VM; the blacksmith branch targets ephemeral
|
|
# runners and carries none of those predicates.
|
|
runs-on: >-
|
|
${{ vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-4vcpu-ubuntu-2404'
|
|
|| vars.DSH_CI_FAILOVER_LINUX == 'selfhosted'
|
|
&& github.event.pull_request.head.repo.full_name == github.repository
|
|
&& github.event.pull_request.head.repo.fork == false
|
|
&& github.event.pull_request.user.login != 'dependabot[bot]'
|
|
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|
|
|| matrix.runner }}
|
|
name: ${{ matrix.name }}
|
|
env:
|
|
DSH_GATE_CONCURRENCY: ${{ matrix.gate_concurrency }}
|
|
DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
|
|
# A failed smoke aborts the remaining compatibility gates instead of
|
|
# letting the build-backed legs run against an already-red aggregate.
|
|
DSH_GATE_FAIL_FAST: '1'
|
|
strategy:
|
|
fail-fast: false
|
|
max-parallel: 1
|
|
matrix:
|
|
include:
|
|
# Pin the supported floor, whose internal loader is v1. Primary
|
|
# jobs track latest 24 and exercise v2, introduced in 24.12.0.
|
|
- node: '24.9'
|
|
name: node 24.9
|
|
runner: ubuntu-latest
|
|
gate_concurrency: '1'
|
|
- node: 26
|
|
name: node 26
|
|
runner: ubuntu-latest
|
|
gate_concurrency: '1'
|
|
steps:
|
|
- uses: actions/checkout@v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# Shared hosts keep version installs and generated caches inside runner temp.
|
|
- name: Isolate compatibility caches
|
|
if: runner.environment == 'self-hosted'
|
|
run: |
|
|
echo "NODE_COMPILE_CACHE=$RUNNER_TEMP/node-compile-cache" >> "$GITHUB_ENV"
|
|
echo "npm_config_devdir=$RUNNER_TEMP/node-gyp" >> "$GITHUB_ENV"
|
|
echo "PNPM_CONFIG_STORE_DIR=$HOME/.local/share/pnpm/store" >> "$GITHUB_ENV"
|
|
|
|
- uses: pnpm/action-setup@v5.0.0
|
|
with:
|
|
dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }}
|
|
|
|
- uses: actions/setup-node@v7.0.0
|
|
env:
|
|
# The runner overwrites RUNNER_* step env before starting JavaScript actions.
|
|
NODE_OPTIONS: ${{ runner.environment == 'self-hosted' && '--import=./scripts/ci-compatible-toolcache.mjs' || '' }}
|
|
with:
|
|
node-version: ${{ matrix.node }}
|
|
cache: ${{ runner.environment == 'github-hosted' && 'pnpm' || '' }}
|
|
package-manager-cache: false
|
|
|
|
- name: Verify isolated Node installation
|
|
if: runner.environment == 'self-hosted'
|
|
run: |
|
|
node_path=$(node -p process.execPath)
|
|
echo "Node executable: $node_path"
|
|
case "$node_path" in
|
|
"$RUNNER_TEMP/node-compat-toolcache/"*) ;;
|
|
*) echo "::error::Node compatibility installation is outside runner temp"; exit 1 ;;
|
|
esac
|
|
|
|
- name: Install (immutable)
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Prepare bubblewrap (unrestrict userns)
|
|
run: bash scripts/prepare-ci-bubblewrap.sh
|
|
|
|
- name: Run compatibility smokes
|
|
env:
|
|
DSH_BUILD_CLIENT_PROFILE: official
|
|
run: pnpm run check:node-compat
|
|
|
|
# Kept out of the gate aggregate: the shape a Node release carries only
|
|
# changes with the Node version, so this belongs to the version matrix
|
|
# rather than to every commit's checks.
|
|
- name: Check Loader internal shape detection
|
|
run: pnpm exec vitest run packages/boot/app-boot/tests/loader-shape.compat.spec.ts
|
|
|
|
# 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`. `needs` cannot cross workflow files: native Windows jobs report
|
|
# the separate "windows checks passed" verdict from ci-windows.yml, Python
|
|
# jobs report "python checks passed" from ci-python.yml, and Wine plus the
|
|
# master-only runtime targets live in ci-master.yml.
|
|
# An explicit status function runs the verdict after failed/skipped needs
|
|
# without keeping a cancelled workflow alive for an obsolete verdict.
|
|
all-checks-passed:
|
|
name: all checks passed
|
|
# This bookkeeping-only verdict must not depend on custom-pool
|
|
# provisioning — and under Linux failover it follows the same selector as
|
|
# the worker jobs it aggregates, so a standard-hosted outage cannot strand
|
|
# the branch-protection verdict either. It retargets with the Linux switch
|
|
# (DSH_CI_FAILOVER_LINUX), not the Windows one, because it aggregates the
|
|
# required Linux workers and runs on the vm-backup pool. Under the
|
|
# 'blacksmith' value the verdict shares Blacksmith's pool with its workers
|
|
# through the same selector, so a Blacksmith outage strands both together —
|
|
# the accepted consequence of the explicit opt-in switch.
|
|
runs-on: >-
|
|
${{ vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-4vcpu-ubuntu-2404'
|
|
|| vars.DSH_CI_FAILOVER_LINUX == '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-bench, node-24-consumers, node-compat]
|
|
if: ${{ !cancelled() && 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, ', ') }})"
|