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
3 changes: 2 additions & 1 deletion .github/workflows/argos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ jobs:

- name: Install dependencies
# Frozen, so the lockfile pins what runs in a job holding ARGOS_TOKEN.
run: pnpm install --frozen-lockfile
# --no-runtime: Setup Node already put the pinned Node.js on PATH.
run: pnpm install --frozen-lockfile --no-runtime
working-directory: lib

- name: Install browsers
Expand Down
38 changes: 27 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,25 @@ jobs:
with:
persist-credentials: false

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: package.json

# No setup-node here, unlike every other job: the self-host installer
# step below runs its own `pnpm install`, which provisions pnpm's pinned
# runtime (devEngines.runtime) whatever is on PATH. So this job takes its
# one Node.js from that install and puts it on PATH for the steps that
# call `node` directly.
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Put the pinned Node.js on PATH
run: |
set -euo pipefail
dir="$(pnpm exec node -p 'require("node:path").dirname(process.execPath)')"
pin="v$("$dir/node" -p 'require("./package.json").devEngines.runtime.version')"
actual="$("$dir/node" --version)"
[ "$actual" = "$pin" ] || { echo "::error::pnpm exec node is $actual, not the pinned $pin"; exit 1; }
echo "$dir" >> "$GITHUB_PATH"

# The disclosed dependency snapshots are a `FAIL IF` in docs/specs/security-supply-chain.md
# (Disclosure), but until this step existed the only thing
# that ever ran the generator was the nightly security audit — strictly
Expand Down Expand Up @@ -169,8 +179,10 @@ jobs:

- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

# --no-runtime: setup-node above already put the pinned Node.js on PATH,
# so pnpm does not download the same devEngines.runtime from nodejs.org.
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile --no-runtime

# browser-launch-env.test.ts skips its real POSIX shell cases without zsh.
- name: Install zsh (browser shell tests)
Expand Down Expand Up @@ -258,9 +270,10 @@ jobs:

- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

# Frozen for the same supply-chain reason as every other job here.
# Frozen for the same supply-chain reason as every other job here;
# --no-runtime as in Library Tests.
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile --no-runtime

# `playwright-core` ships no browser, deliberately — a unit-test run should
# not drag one down. Fetch it here, where it is actually used.
Expand Down Expand Up @@ -344,9 +357,11 @@ jobs:
echo "::error::apt failed after 3 attempts across mirrors"
exit 1

# Frozen for the same supply-chain reason as every other job here.
# Frozen for the same supply-chain reason as every other job here;
# --no-runtime as in Library Tests, which also leaves build.rs the
# setup-node binary on PATH that it checks against the pin.
- name: Install npm dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile --no-runtime
working-directory: standalone

- name: TypeScript check
Expand Down Expand Up @@ -421,9 +436,10 @@ jobs:

- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

# These small suites need no native addon builds.
# These small suites need no native addon builds; --no-runtime as in
# Library Tests.
- name: Install shell test dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
run: pnpm install --frozen-lockfile --ignore-scripts --no-runtime

- name: Windows browser host and shell tests
run: pnpm --dir lib exec vitest run src/host/browser-launch-env.test.ts src/host/agent-browser-host.test.ts src/lib/shell-escape.test.ts
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/hosted-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ jobs:
with:
node-version-file: package.json
cache: pnpm
- run: pnpm install --frozen-lockfile
# --no-runtime: setup-node already put the pinned Node.js on PATH.
- run: pnpm install --frozen-lockfile --no-runtime
- run: pnpm test:hosted
- run: pnpm build:hosted
# Every Worker's static files, each under its own `dist/<worker>/`.
Expand Down Expand Up @@ -111,7 +112,8 @@ jobs:
with:
node-version-file: package.json
cache: pnpm
- run: pnpm install --frozen-lockfile
# --no-runtime: setup-node already put the pinned Node.js on PATH.
- run: pnpm install --frozen-lockfile --no-runtime
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: preview-assets
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/security-audit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,9 @@ jobs:

- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

# --no-runtime: setup-node already put the pinned Node.js on PATH.
- name: Install workspace dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile --no-runtime

- name: Verify AUDIT_PAT is provisioned
env:
Expand Down Expand Up @@ -718,8 +719,9 @@ jobs:

- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

# --no-runtime: setup-node already put the pinned Node.js on PATH.
- name: Install workspace dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile --no-runtime

# In this runner's checkout only: the seeds, then one fresh root commit in
# place of the history, so no diff tells the auditor which changes are
Expand Down
1 change: 1 addition & 0 deletions docs/specs/security-supply-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ The Windows runtime's PE Subsystem field is patched from console (3) to GUI (2)
- **FAIL IF** `S258` `standalone/src-tauri/build.rs` no longer runs `--version` on the binary it is about to bundle and fails the build unless it matches `package.json`'s `devEngines.runtime.version`, or if the check is skipped for any configuration the release matrix builds. One deliberate skip is permitted: `verify_node_version` cannot execute a foreign-arch binary, so it warns and returns when `host != target` — acceptable only while every entry in `release.yml`'s standalone matrix builds on a runner of its own target. A cross-compiled entry, which `resolve_node_binary` admits only when `DORMOUSE_NODE_BINARY` or `NODE_BINARY` names the binary, would bundle it unverified and fails this check.
- **FAIL IF** `S259` `standalone/src-tauri/build.rs` patches the Windows runtime other than after `verify_node_version` returns, changes anything but its two-byte PE Subsystem field, or patches a starting value other than console (3).
- **FAIL IF** `S260` the `build-standalone` job in `.github/workflows/release.yml` does not install the pinned runtime via `node-version-file: package.json`, **or** the root `package.json` gains a `volta.node` or `engines.node` field — alternate version declarations are forbidden (rationale). Other jobs may pin `node-version` inline since their interpreter is never bundled. Pinned by `scripts/supply-chain-config.test.mjs`.
- **FAIL IF** `S278` a step of the `build-standalone` job in `.github/workflows/release.yml` passes `--no-runtime` to pnpm, whose runtime, checked against the lockfile, is the binary the build bundles (rationale). Pinned by `scripts/supply-chain-config.test.mjs`.

Source of truth: `bundle_node_runtime` / `verify_node_version` in `standalone/src-tauri/build.rs`; `getBundledRuntimeDependencies` in `website/scripts/generate-deps.js`.

Expand Down
2 changes: 2 additions & 0 deletions docs/specs/security-supply-chain.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Why the Windows subsystem byte is flipped: leaving the bundled `node.exe` as a c

Why alternate version declarations are excluded: in the pinned [setup-node implementation](https://github.com/actions/setup-node/blob/820762786026740c76f36085b0efc47a31fe5020/src/util.ts), `volta.node` overrides `devEngines.runtime`; `engines.node` is a lower-precedence fallback. Forbidding both keeps one version declaration, though only Volta overrides the current pin. The build's version check rejects a mismatched binary rather than silently bundling it.

Why the release build keeps pnpm's runtime: `build.rs` bundles the `node` on `PATH`, which under `pnpm tauri` is the `devEngines.runtime` install, fetched from nodejs.org and checked against the sha256 in `pnpm-lock.yaml`. PR workflows install with `--no-runtime` so a job downloads Node once, and run on setup-node's binary instead, which comes from the `actions/node-versions` releases (GitHub's repackaging of the nodejs.org build) with no check against the lockfile. Either passes the version check, so only this rule tells them apart. The release pays a second download and a nodejs.org dependency for that.

## Cooldown and alerts

What the `minimumReleaseAge` package rules are: the Renovate equivalent of the pnpm dependency cooldown window, applied per manager.
Expand Down
2 changes: 1 addition & 1 deletion scripts/security-rule-ids.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"next": 278,
"next": 279,
"retired": [
265
]
Expand Down
4 changes: 4 additions & 0 deletions scripts/supply-chain-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ function check(files) {
if (setup.length === 0 || setup.some((s) => s.with?.['node-version-file'] !== 'package.json' || s.with?.['node-version'] !== undefined)) {
fail('release.yml build-standalone does not install Node from node-version-file: package.json');
}
if ((release.jobs?.['build-standalone']?.steps ?? []).some((s) => /--no-runtime\b/.test(s.run ?? ''))) {
fail('release.yml build-standalone installs pnpm without its runtime (--no-runtime)');
}

// The CI linters outside the lockfile: an exact version, verified by hash.
const zizmor = files.zizmor.split('\n').filter((l) => l.trim() && !l.trim().startsWith('#')).join(' ').replace(/\\\s+/g, ' ');
Expand Down Expand Up @@ -105,6 +108,7 @@ for (const [name, mutate, expected] of [
['a volta pin added', json('pkg', (p) => { p.volta = { node: '24.0.0' }; }), 'declares volta.node'],
['an engines pin added', json('pkg', (p) => { p.engines = { node: '>=24' }; }), 'declares engines.node'],
['the release build pins Node inline', text('release', ' with:\n node-version-file: package.json', ' with:\n node-version: 24'), 'node-version-file: package.json'],
['the release build skips the pnpm runtime', text('release', 'run: pnpm install --frozen-lockfile\n', 'run: pnpm install --frozen-lockfile --no-runtime\n'), '--no-runtime'],
['zizmor unhashed', (files) => { files.zizmor = 'zizmor==1.30.1\n'; }, 'does not pin zizmor exactly'],
['zizmor by range', (files) => { files.zizmor = files.zizmor.replace(/zizmor==[\d.]+/, 'zizmor>=1.30'); }, 'does not pin zizmor exactly'],
['zizmor installed without hashes', text('ci', '--require-hashes ', ''), 'does not install zizmor'],
Expand Down
Loading