Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/actions/node-workspace/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Node workspace setup
description: >-
Node 26 + npm 12 + the lockfile-keyed installed node_modules tree, shared by
every job in js-tests.yml. One definition so the jobs cannot drift onto
different cache keys (a drift turns one job's 8s restore into a full
`npm ci`). Run actions/checkout first.

runs:
using: composite
steps:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 26
cache: npm

- name: grab npm 12
shell: bash
run: |
# No-op once the bundled npm is already 12.x — saves ~5-15s/job and
# keeps the installed major aligned with the npm12 cache-key tag.
npm --version | grep -q '^12\.' || npm i -g npm@12

# The ``cache: npm`` option of ``setup-node`` caches only the ~/.npm
# tarball cache. The job then extracts the full workspace node_modules
# again and runs the postinstalls again, which includes the Electron
# binary fetch. This caches the installed tree itself, keyed on the
# lockfile, and skips ``npm ci`` on an exact hit. There are no
# restore-keys: a partial hit leaves a stale tree, so anything other
# than an exact lockfile match reinstalls from the start.
#
# This install runs WITH scripts, so the tree holds the postinstall
# artifacts. The postinstall of electron unpacks its binary into
# node_modules/electron/dist, which is inside the cached tree.
#
# The ~/.cache/electron download cache stays out of the key on purpose.
# ``npm ci`` is skipped on a hit, so nothing reads that cache. It only
# makes the archive larger.
- name: Restore node_modules
id: node-modules-cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
node_modules
apps/*/node_modules
ui-tui/node_modules
ui-tui/packages/*/node_modules
tests-js/node_modules
web/node_modules
key: node-modules-scripts-${{ runner.os }}-node26-npm12-${{ hashFiles('package-lock.json') }}

- uses: ./.github/actions/retry
if: steps.node-modules-cache.outputs.cache-hit != 'true'
with:
command: npm ci
16 changes: 15 additions & 1 deletion .github/scripts/run-workspace-checks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
//
// This also runs on a laptop: `node .github/scripts/run-workspace-checks.mjs`.
// `--concurrency N` sets the limit. `--list` prints the units and exits.
// `--skip <pkg>::<script>` leaves one unit out (repeatable).

import { execFileSync, spawn } from 'node:child_process'
import { availableParallelism } from 'node:os'
Expand Down Expand Up @@ -76,7 +77,20 @@ function runUnit(unit) {

async function main() {
const argv = process.argv.slice(2)
const units = discoverUnits()
let units = discoverUnits()

// `--skip <pkg>::<script>` (repeatable) drops a unit that another CI job
// runs instead (js-tests.yml shards apps/desktop check:test:ui). A skip
// that matches nothing is an error: after a rename the suite would
// otherwise run in neither job and everything would still report green.
const skips = argv.flatMap((a, i) => (a === '--skip' ? [argv[i + 1]] : []))
for (const skip of skips) {
if (!units.some((u) => `${u.pkg}::${u.script}` === skip)) {
console.error(`::error::--skip ${skip} matches no workspace check unit — refusing to guess.`)
process.exit(1)
}
}
units = units.filter((u) => !skips.includes(`${u.pkg}::${u.script}`))

if (units.length === 0) {
console.error(
Expand Down
74 changes: 30 additions & 44 deletions .github/workflows/js-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,49 +51,7 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 26
cache: npm

- name: grab npm 12
run: |
# No-op once the bundled npm is already 12.x — saves ~5-15s/job and
# keeps the installed major aligned with the npm12 cache-key tag.
npm --version | grep -q '^12\.' || npm i -g npm@12

# The ``cache: npm`` option of ``setup-node`` caches only the ~/.npm
# tarball cache. The job then extracts the full workspace node_modules
# again and runs the postinstalls again, which includes the Electron
# binary fetch. This caches the installed tree itself, keyed on the
# lockfile, and skips ``npm ci`` on an exact hit. There are no
# restore-keys: a partial hit leaves a stale tree, so anything other
# than an exact lockfile match reinstalls from the start.
#
# This install runs WITH scripts, so the tree holds the postinstall
# artifacts. The postinstall of electron unpacks its binary into
# node_modules/electron/dist, which is inside the cached tree.
#
# The ~/.cache/electron download cache stays out of the key on purpose.
# ``npm ci`` is skipped on a hit, so nothing reads that cache. It only
# makes the archive larger.
- name: Restore node_modules
id: node-modules-cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
node_modules
apps/*/node_modules
ui-tui/node_modules
ui-tui/packages/*/node_modules
tests-js/node_modules
web/node_modules
key: node-modules-scripts-${{ runner.os }}-node26-npm12-${{ hashFiles('package-lock.json') }}

- uses: ./.github/actions/retry
if: steps.node-modules-cache.outputs.cache-hit != 'true'
with:
command: npm ci
- uses: ./.github/actions/node-workspace

# Every check runs at the same time. The step fails only after all of
# them finish. There are two reasons this is not ``npm run --ws check``.
Expand All @@ -112,5 +70,33 @@ jobs:
# ``check:*`` script needs no change here. An empty list is an error and
# not an empty run, because an empty run reports green after it checks
# nothing.
#
# ``--skip`` removes the one unit the ``ui-tests`` job below runs
# sharded. The script errors if a skip matches no unit, so a rename of
# that script cannot silently drop the suite from both jobs.
- name: Run all workspace checks
run: node .github/scripts/run-workspace-checks.mjs
run: node .github/scripts/run-workspace-checks.mjs --skip 'apps/desktop::check:test:ui'

ui-tests:
# apps/desktop ``check:test:ui`` was the whole critical path of this
# workflow and of the merge group: merge_group run 36217656078 spent
# 656.7s in it (685 files / 6774 tests) while the other 9 checks were done
# by 04:27, and it pushed the run to 11m26s. vitest's own breakdown was
# import 747s + environment 644s + tests 329s of worker time — per-file
# jsdom/import cost across 3 workers, so it splits linearly across
# runners. ``--shard`` partitions the FILE list deterministically; every
# file runs in exactly one shard, each shard in its own jsdom workers as
# before. Same command as ``check:test:ui`` (``npm run test:ui``) plus the
# shard flag.
name: UI tests (shard ${{ matrix.shard }}/3)
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/node-workspace
- name: vitest ui project, shard ${{ matrix.shard }}/3
run: npm run --prefix apps/desktop test:ui -- --shard=${{ matrix.shard }}/3
Loading