ci.yml 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  1. name: CI
  2. on:
  3. push:
  4. branches: [master]
  5. pull_request:
  6. workflow_dispatch:
  7. inputs:
  8. suite:
  9. description: Manual CI suite to run
  10. required: true
  11. default: larger-runner-benchmark
  12. type: choice
  13. options:
  14. - larger-runner-benchmark
  15. - consolidated-runner-benchmark
  16. # Cancel a superseded run on every event EXCEPT push. A push run carries the two
  17. # self-hosted standby drills, which take longer than the interval between master
  18. # merges, so cancelling supersedes a drill before it reaches a verdict and the
  19. # lane yields no readiness evidence. Must be decided here: cancellation applies
  20. # to the whole superseded run, so a job-level group cannot exempt its job.
  21. # Negated rather than `== 'pull_request'` so workflow_dispatch keeps cancelling:
  22. # a re-dispatched runner benchmark holds up to 12 larger runners for 15 minutes
  23. # and shares this group with the drills on master, so queueing it would delay
  24. # them. The guarantee is narrow — evaluated against the newly triggered run, so a
  25. # dispatch on master still cancels a mid-flight drill, and a newer pending push
  26. # displaces an older one. Bounds and rationale:
  27. # .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
  28. concurrency:
  29. group: ${{ github.workflow }}-${{ github.ref }}
  30. cancel-in-progress: ${{ github.event_name != 'push' }}
  31. permissions:
  32. contents: read
  33. env:
  34. PRIMARY_NODE_VERSION: '24'
  35. # CI runs must never report to the production telemetry endpoint baked
  36. # into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
  37. DSH_TELEMETRY_DISABLED: '1'
  38. jobs:
  39. # TODO(hosted-serial-ci): Re-enable the three hosted serial reference jobs before release.
  40. # The self-hosted standby remains active on every master push.
  41. # Three enterprise jobs isolate coverage, static analysis, and the
  42. # build-backed consumer tail. The consumer job owns the only Linux build so
  43. # all three jobs enter runner allocation independently.
  44. #
  45. # FAILOVER: each Linux enterprise job resolves its pool through the
  46. # DSH_CI_FAILOVER repository variable. Unset (normal), the expressions
  47. # pick the hosted enterprise pools below. Setting the variable to
  48. # 'selfhosted' (repo Settings → Actions → Variables; writer-manageable
  49. # repository state — not PR-editable, no merge required) retargets all
  50. # three onto the in-house
  51. # vm-backup pool and re-running the failed jobs is the entire switch —
  52. # see .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md. The
  53. # in-house pool's readiness is re-proven on every master push by the
  54. # serial-linux-selfhosted standby lane below.
  55. node-24:
  56. if: github.event_name == 'pull_request'
  57. runs-on: >-
  58. ${{ vars.DSH_CI_FAILOVER == 'selfhosted'
  59. && github.event.pull_request.user.login != 'dependabot[bot]'
  60. && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
  61. || 'dsh-ubuntu-24-04-16core' }}
  62. name: node 24 / static
  63. env:
  64. DSH_GATE_CONCURRENCY: '8'
  65. steps:
  66. # Fetch complete history so the archive gate can read the trusted PR base from a reused shallow checkout.
  67. - uses: actions/checkout@v6
  68. with:
  69. fetch-depth: 0
  70. persist-credentials: false
  71. - uses: pnpm/action-setup@v4
  72. with:
  73. dest: ${{ runner.temp }}/setup-pnpm
  74. - uses: actions/setup-node@v6
  75. with:
  76. node-version: ${{ env.PRIMARY_NODE_VERSION }}
  77. - name: Configure pnpm store path
  78. id: pnpm-store
  79. run: |
  80. store_root="$HOME/.local/share/pnpm/store"
  81. echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
  82. store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
  83. echo "path=$store_path" >> "$GITHUB_OUTPUT"
  84. # Pull requests consume the default-branch cache but do not put cache
  85. # compression and upload on the paid latency-critical path. Skipped
  86. # under failover — see the coverage lane's identical rationale.
  87. - uses: actions/cache/restore@v4
  88. if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
  89. with:
  90. path: ${{ steps.pnpm-store.outputs.path }}
  91. key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
  92. restore-keys: |
  93. ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
  94. - name: Install (immutable)
  95. run: pnpm install --frozen-lockfile
  96. - name: Run static gates
  97. env:
  98. DSH_ARCHIVE_BASE_REF: ${{ github.event.pull_request.base.sha }}
  99. run: pnpm run check:ci:static
  100. node-24-coverage:
  101. if: github.event_name == 'pull_request'
  102. runs-on: >-
  103. ${{ vars.DSH_CI_FAILOVER == 'selfhosted'
  104. && github.event.pull_request.user.login != 'dependabot[bot]'
  105. && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
  106. || 'dsh-ubuntu-24-04-16core' }}
  107. name: node 24 / coverage
  108. env:
  109. # The hosted 16-core runner uses six coverage workers. The failover pool
  110. # shares one 64-core VM across six always-on runner instances, so each
  111. # instance may use eight while keeping the worst case at 8 × 6 = 48
  112. # workers; process-bound suites remain isolated in forks.
  113. DSH_COVERAGE_MAX_WORKERS: ${{ vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '6' }}
  114. DSH_GATE_CONCURRENCY: '3'
  115. steps:
  116. - uses: actions/checkout@v6
  117. with:
  118. persist-credentials: false
  119. - uses: pnpm/action-setup@v4
  120. with:
  121. dest: ${{ runner.temp }}/setup-pnpm
  122. - uses: actions/setup-node@v6
  123. with:
  124. node-version: ${{ env.PRIMARY_NODE_VERSION }}
  125. - name: Configure pnpm store path
  126. id: pnpm-store
  127. run: |
  128. store_root="$HOME/.local/share/pnpm/store"
  129. echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
  130. store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
  131. echo "path=$store_path" >> "$GITHUB_OUTPUT"
  132. # Skipped under failover: the self-hosted VM's persistent pnpm store
  133. # already serves warm installs, while restoring the hosted archive
  134. # would spend ~52 s pulling ~180 MB into that populated store.
  135. - uses: actions/cache/restore@v4
  136. if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
  137. with:
  138. path: ${{ steps.pnpm-store.outputs.path }}
  139. key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
  140. restore-keys: |
  141. ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
  142. - name: Install dependencies and prepare bubblewrap
  143. run: |
  144. pnpm install --frozen-lockfile &
  145. install_pid=$!
  146. bash scripts/prepare-ci-bubblewrap.sh &
  147. sandbox_pid=$!
  148. install_status=0
  149. wait "$install_pid" || install_status=$?
  150. sandbox_status=0
  151. wait "$sandbox_pid" || sandbox_status=$?
  152. if (( install_status != 0 )); then exit "$install_status"; fi
  153. exit "$sandbox_status"
  154. - name: Run exhaustive coverage
  155. run: pnpm run check:ci:coverage
  156. node-24-consumers:
  157. if: github.event_name == 'pull_request'
  158. runs-on: >-
  159. ${{ vars.DSH_CI_FAILOVER == 'selfhosted'
  160. && github.event.pull_request.user.login != 'dependabot[bot]'
  161. && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
  162. || 'dsh-ubuntu-24-04-16core' }}
  163. name: node 24 / snapshots and artifacts
  164. env:
  165. DSH_GATE_CONCURRENCY: '8'
  166. DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
  167. DSH_OXLINT_THREADS: '8'
  168. DSH_PUBLINT_CONCURRENCY: '8'
  169. # Failover halves snapshot concurrency for the shared 64-core VM.
  170. DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '12' || '32' }}
  171. steps:
  172. - uses: actions/checkout@v6
  173. with:
  174. persist-credentials: false
  175. - uses: pnpm/action-setup@v4
  176. with:
  177. dest: ${{ runner.temp }}/setup-pnpm
  178. - uses: actions/setup-node@v6
  179. with:
  180. node-version: ${{ env.PRIMARY_NODE_VERSION }}
  181. - name: Configure pnpm store path
  182. id: pnpm-store
  183. run: |
  184. store_root="$HOME/.local/share/pnpm/store"
  185. echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
  186. store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
  187. echo "path=$store_path" >> "$GITHUB_OUTPUT"
  188. # Skipped under failover — see the coverage lane's identical rationale.
  189. - uses: actions/cache/restore@v4
  190. if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
  191. with:
  192. path: ${{ steps.pnpm-store.outputs.path }}
  193. key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
  194. restore-keys: |
  195. ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
  196. # Pull requests restore the cache normally produced by serial-linux on
  197. # master; they do not pay compression and upload on the required path.
  198. - uses: actions/cache/restore@v4
  199. if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
  200. with:
  201. path: ~/.cache/ms-playwright
  202. key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
  203. restore-keys: |
  204. ${{ runner.os }}-playwright-
  205. - name: Install dependencies and prepare bubblewrap
  206. run: |
  207. pnpm install --frozen-lockfile &
  208. install_pid=$!
  209. bash scripts/prepare-ci-bubblewrap.sh &
  210. sandbox_pid=$!
  211. install_status=0
  212. wait "$install_pid" || install_status=$?
  213. sandbox_status=0
  214. wait "$sandbox_pid" || sandbox_status=$?
  215. if (( install_status != 0 )); then exit "$install_status"; fi
  216. exit "$sandbox_status"
  217. - name: Install Playwright Chromium and hosted dependencies
  218. if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
  219. run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps chromium
  220. # The persistent VM image owns Playwright's Linux system packages; do
  221. # not mutate the shared host with apt on every failover run.
  222. - name: Install Playwright Chromium on the failover VM
  223. if: vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]'
  224. run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium
  225. - name: Run compatibility, snapshot, and artifact gates
  226. run: pnpm run check:ci:consumers
  227. node-compat:
  228. if: github.event_name == 'pull_request'
  229. # Each compatibility contract receives an independent standard hosted job.
  230. runs-on: ${{ matrix.runner }}
  231. name: ${{ matrix.name }}
  232. env:
  233. DSH_GATE_CONCURRENCY: ${{ matrix.gate_concurrency }}
  234. DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
  235. strategy:
  236. fail-fast: false
  237. matrix:
  238. include:
  239. - node: '22.19'
  240. name: node 22.19
  241. runner: ubuntu-latest
  242. gate_concurrency: '1'
  243. - node: 26
  244. name: node 26
  245. runner: ubuntu-latest
  246. gate_concurrency: '1'
  247. steps:
  248. - uses: actions/checkout@v6
  249. - uses: pnpm/action-setup@v4
  250. with:
  251. dest: ${{ runner.temp }}/setup-pnpm
  252. - uses: actions/setup-node@v6
  253. with:
  254. node-version: ${{ matrix.node }}
  255. cache: pnpm
  256. - name: Install (immutable)
  257. run: pnpm install --frozen-lockfile
  258. - name: Run compatibility smokes
  259. run: pnpm run check:node-compat
  260. python-sdk:
  261. if: github.event_name == 'pull_request'
  262. runs-on: ubuntu-latest
  263. name: python 3.10 / keyless SDK
  264. steps:
  265. - uses: actions/checkout@v6
  266. - uses: actions/setup-python@v6.3.0
  267. with:
  268. python-version: '3.10'
  269. cache: pip
  270. - name: Install uv
  271. run: python -m pip install uv==0.11.23
  272. - name: Run complete keyless Python suite
  273. run: uv run --python 3.10 --group test --project python/sdk pytest
  274. # One native target makes the complete release-shaped Python path required
  275. # without duplicating platform-independent behavior across the release matrix.
  276. # The reusable builder owns the executable, snapshot, wheel, clean-install,
  277. # GLIBC, and manylinux checks; release validation retains all native targets.
  278. python-runtime:
  279. if: github.event_name == 'pull_request'
  280. name: python runtime / release-shaped Linux x64
  281. uses: ./.github/workflows/build-exe-for-python-sdk.yml
  282. with:
  283. targets: node24-linux-x64
  284. ci: true
  285. # The required pull-request Windows signal: the two blocking win32 surfaces
  286. # (workspace build, production site) execute with real, checksum-verified
  287. # Windows Node under Wine on standard hosted Linux. The independent
  288. # windows-native job below keeps the complete native-kernel inventory —
  289. # including the observational portability gates this lane does not run —
  290. # on real Windows. This job only provisions runner state (caches,
  291. # apt); scripts/wine-windows-gates.sh owns the gate logic and is the same
  292. # script the optional local gate `pnpm run check:windows-wine` runs.
  293. # Current topology and fidelity limits live in
  294. # .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
  295. windows:
  296. if: github.event_name == 'pull_request'
  297. runs-on: ubuntu-latest
  298. name: windows node 24 / wine blocking
  299. timeout-minutes: 15
  300. steps:
  301. - uses: actions/checkout@v6
  302. with:
  303. persist-credentials: false
  304. - uses: pnpm/action-setup@v4
  305. with:
  306. dest: ${{ runner.temp }}/setup-pnpm
  307. - uses: actions/setup-node@v6
  308. with:
  309. node-version: ${{ env.PRIMARY_NODE_VERSION }}
  310. - name: Configure pnpm store path
  311. id: pnpm-store
  312. run: |
  313. store_root="$HOME/.local/share/pnpm/store"
  314. echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
  315. store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
  316. echo "path=$store_path" >> "$GITHUB_OUTPUT"
  317. - uses: actions/cache/restore@v4
  318. with:
  319. path: ${{ steps.pnpm-store.outputs.path }}
  320. key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
  321. restore-keys: |
  322. ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
  323. # Master's wine-apt-cache job seeds the default-branch scope every pull
  324. # request can read; a save from this job only reaches reruns of the
  325. # same merge ref.
  326. - name: Compose Wine apt cache key
  327. id: wine-cache-key
  328. run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT"
  329. - uses: actions/cache@v4
  330. with:
  331. path: ~/wine-debs
  332. key: ${{ steps.wine-cache-key.outputs.key }}
  333. # Runner provisioning only — a developer machine installs Wine through
  334. # its own package manager; the gate script assumes a wine binary and
  335. # fails loud without one. Wine from the apt cache when present; else
  336. # download the full dependency closure once and keep it for the next
  337. # run. The `wine` dispatcher package (not bare `wine64`) is what puts a
  338. # binary on PATH.
  339. - name: Install Wine
  340. run: |
  341. if compgen -G "$HOME/wine-debs/*.deb" > /dev/null; then
  342. sudo apt-get install -y --no-install-recommends "$HOME"/wine-debs/*.deb
  343. else
  344. sudo apt-get update
  345. sudo apt-get install -y --no-install-recommends --download-only wine
  346. mkdir -p "$HOME/wine-debs"
  347. cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/" 2>/dev/null || true
  348. sudo apt-get install -y --no-install-recommends wine
  349. fi
  350. - name: Run the Wine Windows gates
  351. run: bash scripts/wine-windows-gates.sh
  352. - name: Shut down wineserver
  353. if: always()
  354. run: wineserver -k 2>/dev/null || true
  355. # Master seeds the Wine apt-archive cache in the default-branch scope,
  356. # which every pull request's windows job can restore; saves from
  357. # pull-request runs are scoped to their own merge ref and help nobody
  358. # else. Runs in seconds when the image version already has a cache.
  359. wine-apt-cache:
  360. if: github.event_name == 'push' && github.ref == 'refs/heads/master'
  361. name: wine apt cache
  362. runs-on: ubuntu-latest
  363. timeout-minutes: 10
  364. steps:
  365. - name: Compose Wine apt cache key
  366. id: wine-cache-key
  367. run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT"
  368. - uses: actions/cache@v4
  369. id: wine-cache
  370. with:
  371. path: ~/wine-debs
  372. key: ${{ steps.wine-cache-key.outputs.key }}
  373. - name: Download the Wine dependency closure
  374. if: steps.wine-cache.outputs.cache-hit != 'true'
  375. run: |
  376. sudo apt-get update
  377. sudo apt-get install -y --no-install-recommends --download-only wine
  378. mkdir -p "$HOME/wine-debs"
  379. cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/"
  380. du -sh "$HOME/wine-debs"
  381. # Every pull request also gets a real Windows-kernel signal. This job keeps
  382. # its own unmasked conclusion but is deliberately absent from
  383. # all-checks-passed.needs, so it never delays or changes that required
  384. # verdict. Under normal operation it runs on the hosted larger runner; under
  385. # failover (DSH_CI_FAILOVER=selfhosted) it retargets onto the in-house
  386. # self-hosted Windows pool. Dependabot PRs are excluded from the self-hosted
  387. # pool and stay queued for the hosted runner — see the failover runbook.
  388. windows-native:
  389. if: github.event_name == 'pull_request'
  390. runs-on: >-
  391. ${{ vars.DSH_CI_FAILOVER == 'selfhosted'
  392. && github.event.pull_request.user.login != 'dependabot[bot]'
  393. && fromJSON('["self-hosted", "dsh-win-ci", "windows"]')
  394. || 'dsh-windows-2025-16core' }}
  395. name: windows node 24 / native complete
  396. timeout-minutes: 60
  397. env:
  398. DSH_COVERAGE_MAX_WORKERS: '2'
  399. DSH_GATE_CONCURRENCY: '2'
  400. DSH_PUBLINT_CONCURRENCY: '8'
  401. steps:
  402. - uses: actions/checkout@v6
  403. with:
  404. persist-credentials: false
  405. - name: Enable Developer Mode (symlink support)
  406. shell: pwsh
  407. run: >-
  408. reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
  409. /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
  410. - uses: pnpm/action-setup@v4
  411. with:
  412. dest: ${{ runner.temp }}/setup-pnpm
  413. - uses: actions/setup-node@v6
  414. with:
  415. node-version: ${{ env.PRIMARY_NODE_VERSION }}
  416. # Extracting the many-file pnpm store cache is slower than a clean
  417. # install on hosted Windows runners, and saving it adds latency after
  418. # the gates. The self-hosted VM's persistent store makes caching
  419. # redundant.
  420. - name: Install (immutable)
  421. shell: pwsh
  422. run: pnpm install --frozen-lockfile
  423. - name: Run complete native Windows gate inventory
  424. shell: pwsh
  425. run: pnpm run check:ci:windows-complete
  426. # The hosted reference jobs below are temporarily disabled; the self-hosted
  427. # standby remains active. Each enabled host executes the complete, unsharded
  428. # primary Node aggregate with one gate worker, giving reviewers a simple
  429. # cross-platform oracle for completeness and timing.
  430. serial-linux:
  431. if: false
  432. name: serial / linux
  433. runs-on: ubuntu-latest
  434. steps:
  435. - uses: actions/checkout@v6
  436. with:
  437. fetch-depth: 2
  438. - uses: pnpm/action-setup@v4
  439. with:
  440. dest: ${{ runner.temp }}/setup-pnpm
  441. - uses: actions/setup-node@v6
  442. with:
  443. node-version: ${{ env.PRIMARY_NODE_VERSION }}
  444. - name: Configure pnpm store path
  445. id: pnpm-store
  446. run: |
  447. store_root="$HOME/.local/share/pnpm/store"
  448. echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
  449. store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
  450. echo "path=$store_path" >> "$GITHUB_OUTPUT"
  451. # Master refreshes the pnpm store cache that pull requests restore without saving.
  452. # The store cache stays a hand-rolled actions/cache step rather than
  453. # setup-node's `cache: pnpm`: the enterprise pull-request jobs above
  454. # restore exactly this key and path, and setup-node's built-in cache
  455. # uses its own key format — converting this producer would silently
  456. # starve their documented restore-only optimization.
  457. - uses: actions/cache@v4
  458. with:
  459. path: ${{ steps.pnpm-store.outputs.path }}
  460. key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
  461. restore-keys: |
  462. ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
  463. # Master produces the hosted Chromium cache restored by pull requests.
  464. - uses: actions/cache@v4
  465. with:
  466. path: ~/.cache/ms-playwright
  467. key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
  468. restore-keys: |
  469. ${{ runner.os }}-playwright-
  470. - name: Install (immutable)
  471. run: pnpm install --frozen-lockfile
  472. - name: Install Playwright Chromium and system dependencies
  473. run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps chromium
  474. - name: Prepare bubblewrap (unrestrict userns)
  475. run: bash scripts/prepare-ci-bubblewrap.sh
  476. - name: Run complete unsharded primary Node CI serially
  477. env:
  478. DSH_ARCHIVE_BASE_REF: ${{ github.event.before }}
  479. DSH_COVERAGE_MAX_WORKERS: '1'
  480. DSH_E2E_MAX_WORKERS: '1'
  481. DSH_GATE_CONCURRENCY: '1'
  482. DSH_OXLINT_THREADS: '1'
  483. DSH_PUBLINT_CONCURRENCY: '1'
  484. DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
  485. run: pnpm run check:ci:linux-primary
  486. # Hot-standby drill for the in-house self-hosted pool: every master move
  487. # re-runs the complete unsharded aggregate on the persistent 64-core VM,
  488. # continuously proving that environment can take over a required lane if
  489. # the hosted pools degrade (the switch is then setting the writer-manageable
  490. # DSH_CI_FAILOVER variable — see the failover runbook, no merge required).
  491. # Push-triggered, so this lane always executes the base branch's own
  492. # workflow definition. (Under failover, pull_request jobs do reach these
  493. # runners with the PR merge ref's workflow — the boundary there is
  494. # repository membership: private, forking disabled, Dependabot excluded.)
  495. # Non-blocking for
  496. # pull requests; no cache steps because the VM's persistent pnpm store and
  497. # tool caches make them redundant (and saving here would poison the hosted
  498. # cache namespace with self-hosted paths).
  499. serial-linux-selfhosted:
  500. if: github.event_name == 'push' && github.ref == 'refs/heads/master'
  501. name: serial / linux (self-hosted standby)
  502. runs-on: [self-hosted, linux, x64, vm-backup]
  503. steps:
  504. # Full history + DSH_ARCHIVE_BASE_REF below: same frozen-archive
  505. # comparison as serial-linux. Depth 2 would miss github.event.before
  506. # on multi-commit or force pushes; full fetch is cheap here because
  507. # checkout resolves against the VM's local mirror.
  508. - uses: actions/checkout@v6
  509. with:
  510. fetch-depth: 0
  511. - uses: pnpm/action-setup@v4
  512. with:
  513. dest: ${{ runner.temp }}/setup-pnpm
  514. - uses: actions/setup-node@v6
  515. with:
  516. node-version: ${{ env.PRIMARY_NODE_VERSION }}
  517. - name: Configure persistent pnpm store
  518. run: echo "PNPM_CONFIG_STORE_DIR=$HOME/.local/share/pnpm/store" >> "$GITHUB_ENV"
  519. - name: Install (immutable)
  520. run: pnpm install --frozen-lockfile
  521. # The persistent VM image owns Playwright's Linux system packages; this
  522. # step also proves that browser provisioning remains usable for failover.
  523. - name: Install Playwright Chromium
  524. run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium
  525. - name: Prepare bubblewrap (unrestrict userns)
  526. run: bash scripts/prepare-ci-bubblewrap.sh
  527. - name: Run complete unsharded primary Node CI serially
  528. env:
  529. DSH_ARCHIVE_BASE_REF: ${{ github.event.before }}
  530. DSH_COVERAGE_MAX_WORKERS: '1'
  531. DSH_E2E_MAX_WORKERS: '1'
  532. DSH_GATE_CONCURRENCY: '1'
  533. DSH_OXLINT_THREADS: '1'
  534. DSH_PUBLINT_CONCURRENCY: '1'
  535. DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
  536. run: pnpm run check:ci:linux-primary
  537. serial-macos:
  538. if: false
  539. name: serial / macos
  540. runs-on: macos-latest
  541. steps:
  542. - uses: actions/checkout@v6
  543. - uses: pnpm/action-setup@v4
  544. with:
  545. dest: ${{ runner.temp }}/setup-pnpm
  546. - uses: actions/setup-node@v6
  547. with:
  548. node-version: ${{ env.PRIMARY_NODE_VERSION }}
  549. - name: Install (immutable)
  550. run: pnpm install --frozen-lockfile
  551. - name: Run complete unsharded primary Node CI serially
  552. env:
  553. DSH_COVERAGE_MAX_WORKERS: '1'
  554. DSH_E2E_MAX_WORKERS: '1'
  555. DSH_GATE_CONCURRENCY: '1'
  556. DSH_PUBLINT_CONCURRENCY: '1'
  557. DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
  558. run: pnpm run check:ci
  559. # Hot-standby drill for the in-house self-hosted Windows pool: every master
  560. # move re-runs the complete unsharded Windows gate inventory on the persistent
  561. # VM, continuously proving that environment can take over the required
  562. # `windows` lane if the hosted pool degrades (the switch is setting the
  563. # writer-manageable DSH_CI_FAILOVER variable — see the failover runbook, no
  564. # merge required). Push-triggered, so this lane always executes the base
  565. # branch's own workflow definition. Non-blocking for pull requests; absent
  566. # from all-checks-passed.needs by design — the required `windows` job owns
  567. # the PR verdict. No cache steps because the VM's persistent pnpm store
  568. # and tool caches make them redundant (and saving here would poison the
  569. # hosted cache namespace with self-hosted paths).
  570. serial-windows:
  571. if: github.event_name == 'push' && github.ref == 'refs/heads/master'
  572. name: serial / windows (self-hosted standby)
  573. runs-on: [self-hosted, dsh-win-ci, windows]
  574. timeout-minutes: 60
  575. steps:
  576. - uses: actions/checkout@v6
  577. - name: Enable Developer Mode (symlink support)
  578. shell: pwsh
  579. run: >-
  580. reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
  581. /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
  582. - uses: pnpm/action-setup@v4
  583. with:
  584. dest: ${{ runner.temp }}/setup-pnpm
  585. - uses: actions/setup-node@v6
  586. with:
  587. node-version: ${{ env.PRIMARY_NODE_VERSION }}
  588. - name: Configure persistent pnpm store
  589. shell: pwsh
  590. run: |
  591. $storeRoot = "$env:LOCALAPPDATA\pnpm\store"
  592. echo "PNPM_CONFIG_STORE_DIR=$storeRoot" >> $env:GITHUB_ENV
  593. - name: Install (immutable)
  594. shell: pwsh
  595. run: pnpm install --frozen-lockfile
  596. - name: Run complete unsharded Windows gate inventory serially
  597. shell: pwsh
  598. env:
  599. DSH_COVERAGE_MAX_WORKERS: '1'
  600. DSH_GATE_CONCURRENCY: '1'
  601. DSH_PUBLINT_CONCURRENCY: '1'
  602. run: pnpm run check:ci:windows-complete
  603. # Manual, bounded comparison of the actual critical Linux and Windows lanes.
  604. # The named pools are restricted at the organization level to this repository.
  605. larger-runner-benchmark:
  606. if: github.event_name == 'workflow_dispatch' && inputs.suite == 'larger-runner-benchmark'
  607. runs-on: ${{ matrix.runner }}
  608. timeout-minutes: 15
  609. strategy:
  610. fail-fast: false
  611. max-parallel: 12
  612. matrix:
  613. include:
  614. - platform: linux
  615. cores: '4'
  616. runner: dsh-ubuntu-24-04-4core
  617. workload: typecheck
  618. - platform: linux
  619. cores: '8'
  620. runner: dsh-ubuntu-24-04-8core
  621. workload: typecheck
  622. - platform: linux
  623. cores: '16'
  624. runner: dsh-ubuntu-24-04-16core
  625. workload: typecheck
  626. - platform: linux
  627. cores: '32'
  628. runner: dsh-ubuntu-24-04-32core
  629. workload: typecheck
  630. - platform: linux
  631. cores: '64'
  632. runner: dsh-ubuntu-24-04-64core
  633. workload: typecheck
  634. - platform: linux
  635. cores: '96'
  636. runner: dsh-ubuntu-24-04-96core
  637. workload: typecheck
  638. - platform: windows
  639. cores: '4'
  640. runner: dsh-windows-2025-4core
  641. workload: production-site
  642. - platform: windows
  643. cores: '8'
  644. runner: dsh-windows-2025-8core
  645. workload: production-site
  646. - platform: windows
  647. cores: '16'
  648. runner: dsh-windows-2025-16core
  649. workload: production-site
  650. - platform: windows
  651. cores: '32'
  652. runner: dsh-windows-2025-32core
  653. workload: production-site
  654. - platform: windows
  655. cores: '64'
  656. runner: dsh-windows-2025-64core
  657. workload: production-site
  658. - platform: windows
  659. cores: '96'
  660. runner: dsh-windows-2025-96core
  661. workload: production-site
  662. steps:
  663. - uses: actions/checkout@v6
  664. - uses: pnpm/action-setup@v4
  665. with:
  666. dest: ${{ runner.temp }}/setup-pnpm
  667. # The benchmark's Windows lanes deliberately skip the store cache like
  668. # the independent native Windows job; an empty input disables caching.
  669. - uses: actions/setup-node@v6
  670. with:
  671. node-version: ${{ env.PRIMARY_NODE_VERSION }}
  672. cache: ${{ matrix.platform == 'linux' && 'pnpm' || '' }}
  673. - name: Report runner capacity
  674. run: >-
  675. node -e "const os = require('node:os');
  676. console.log(JSON.stringify({ arch: process.arch, cpus: os.cpus().length,
  677. memoryGiB: Math.round(os.totalmem() / 2 ** 30) }))"
  678. - name: Install (immutable)
  679. run: pnpm install --frozen-lockfile
  680. - name: Run critical Linux typecheck lane
  681. if: matrix.platform == 'linux'
  682. run: pnpm run typecheck
  683. - name: Run critical Windows production-site lane
  684. if: matrix.platform == 'windows'
  685. run: pnpm run docs:build
  686. # Manual comparison of the intended low-fanout topology. Linux runs the
  687. # complete unsharded primary aggregate with bounded in-runner parallelism;
  688. # Windows runs both blocking build targets concurrently through run-gates.
  689. consolidated-runner-benchmark:
  690. if: github.event_name == 'workflow_dispatch' && inputs.suite == 'consolidated-runner-benchmark'
  691. runs-on: ${{ matrix.runner }}
  692. timeout-minutes: 15
  693. strategy:
  694. fail-fast: false
  695. max-parallel: 12
  696. matrix:
  697. include:
  698. - platform: linux
  699. cores: '4'
  700. runner: dsh-ubuntu-24-04-4core
  701. workers: '4'
  702. - platform: linux
  703. cores: '8'
  704. runner: dsh-ubuntu-24-04-8core
  705. workers: '8'
  706. - platform: linux
  707. cores: '16'
  708. runner: dsh-ubuntu-24-04-16core
  709. workers: '16'
  710. - platform: linux
  711. cores: '32'
  712. runner: dsh-ubuntu-24-04-32core
  713. workers: '32'
  714. - platform: linux
  715. cores: '64'
  716. runner: dsh-ubuntu-24-04-64core
  717. workers: '32'
  718. - platform: linux
  719. cores: '96'
  720. runner: dsh-ubuntu-24-04-96core
  721. workers: '32'
  722. - platform: windows
  723. cores: '4'
  724. runner: dsh-windows-2025-4core
  725. workers: '2'
  726. - platform: windows
  727. cores: '8'
  728. runner: dsh-windows-2025-8core
  729. workers: '2'
  730. - platform: windows
  731. cores: '16'
  732. runner: dsh-windows-2025-16core
  733. workers: '2'
  734. - platform: windows
  735. cores: '32'
  736. runner: dsh-windows-2025-32core
  737. workers: '2'
  738. - platform: windows
  739. cores: '64'
  740. runner: dsh-windows-2025-64core
  741. workers: '2'
  742. - platform: windows
  743. cores: '96'
  744. runner: dsh-windows-2025-96core
  745. workers: '2'
  746. steps:
  747. - uses: actions/checkout@v6
  748. - uses: pnpm/action-setup@v4
  749. with:
  750. dest: ${{ runner.temp }}/setup-pnpm
  751. # Unlike the larger-runner suite, both platforms cache the store here:
  752. # the consolidated topology measures cache mechanics as workload.
  753. - uses: actions/setup-node@v6
  754. with:
  755. node-version: ${{ env.PRIMARY_NODE_VERSION }}
  756. cache: pnpm
  757. - name: Report runner capacity
  758. run: >-
  759. node -e "const os = require('node:os');
  760. console.log(JSON.stringify({ arch: process.arch, cpus: os.cpus().length,
  761. memoryGiB: Math.round(os.totalmem() / 2 ** 30) }))"
  762. - name: Install and prepare Linux
  763. if: matrix.platform == 'linux'
  764. run: |
  765. pnpm install --frozen-lockfile &
  766. install_pid=$!
  767. bash scripts/prepare-ci-bubblewrap.sh &
  768. sandbox_pid=$!
  769. install_status=0
  770. wait "$install_pid" || install_status=$?
  771. sandbox_status=0
  772. wait "$sandbox_pid" || sandbox_status=$?
  773. if (( install_status != 0 )); then exit "$install_status"; fi
  774. exit "$sandbox_status"
  775. - name: Install (immutable)
  776. if: matrix.platform == 'windows'
  777. shell: pwsh
  778. run: pnpm install --frozen-lockfile
  779. - name: Run complete unsharded primary Node CI concurrently
  780. if: matrix.platform == 'linux'
  781. env:
  782. DSH_COVERAGE_MAX_WORKERS: ${{ matrix.workers }}
  783. DSH_GATE_CONCURRENCY: ${{ matrix.workers }}
  784. DSH_OXLINT_THREADS: ${{ matrix.workers }}
  785. DSH_PUBLINT_CONCURRENCY: ${{ matrix.workers }}
  786. DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ matrix.workers }}
  787. run: pnpm run check:ci
  788. - name: Run blocking Windows builds concurrently
  789. if: matrix.platform == 'windows'
  790. shell: pwsh
  791. env:
  792. DSH_GATE_CONCURRENCY: ${{ matrix.workers }}
  793. run: pnpm run check:ci:windows-blocking
  794. # Single stable required check for branch protection: require "all checks
  795. # passed" instead of enumerating matrix legs whose names change as lanes and
  796. # node versions evolve. Every blocking job in THIS workflow must be listed in
  797. # `needs`. The required Wine job is listed as `windows`; `windows-native` is
  798. # deliberately absent so its independent result never delays or changes this
  799. # verdict. (`needs` cannot reach across workflow files; e2e.yml stays its own
  800. # check.)
  801. # `if: always()` is load-bearing: without it a failed dependency
  802. # would SKIP this job, and GitHub counts a skipped required check as passing
  803. # — so this job always runs and fails on any non-success result, including
  804. # 'cancelled' and 'skipped'.
  805. all-checks-passed:
  806. name: all checks passed
  807. # This bookkeeping-only verdict must not depend on custom-pool
  808. # provisioning — and under failover it follows the same selector as the
  809. # worker jobs it aggregates, so a standard-hosted outage cannot strand
  810. # the branch-protection verdict either.
  811. runs-on: >-
  812. ${{ vars.DSH_CI_FAILOVER == 'selfhosted'
  813. && github.event.pull_request.user.login != 'dependabot[bot]'
  814. && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
  815. || 'ubuntu-latest' }}
  816. needs: [node-24, node-24-coverage, node-24-consumers, node-compat, python-sdk, python-runtime, windows]
  817. if: always() && github.event_name == 'pull_request'
  818. steps:
  819. - name: Fail if any needed job did not succeed
  820. if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')
  821. run: |
  822. echo "::error::Needed job results: ${{ join(needs.*.result, ', ') }}"
  823. exit 1
  824. - name: All checks passed
  825. run: echo "All needed jobs succeeded (${{ join(needs.*.result, ', ') }})"