diff --git a/.github/actions/install-node-dependencies/action.yml b/.github/actions/install-node-dependencies/action.yml index de1a0c44933..cff187c9550 100644 --- a/.github/actions/install-node-dependencies/action.yml +++ b/.github/actions/install-node-dependencies/action.yml @@ -274,15 +274,16 @@ runs: printf 'ELECTRON_CACHE=%s\n' "$cache_root" >> "$GITHUB_ENV" printf 'version=%s\n' "$(node -p "require('./node_modules/electron/package.json').version")" >> "$GITHUB_OUTPUT" + # Main and the warmers own every OS's archive; PR copies duplicated it per PR. - name: Cache Electron package archive - if: (github.event_name != 'pull_request' || runner.os != 'Linux') && steps.electron-package-cache.outputs.version != '' + if: github.event_name != 'pull_request' && steps.electron-package-cache.outputs.version != '' uses: actions/cache@v5 with: path: ${{ steps.electron-package-cache.outputs.cache-root }} key: electron-package-${{ runner.os }}-${{ runner.arch }}-${{ steps.electron-package-cache.outputs.version }} - name: Restore Electron package archive without saving - if: github.event_name == 'pull_request' && runner.os == 'Linux' && steps.electron-package-cache.outputs.version != '' + if: github.event_name == 'pull_request' && steps.electron-package-cache.outputs.version != '' uses: actions/cache/restore@v5 with: path: ${{ steps.electron-package-cache.outputs.cache-root }} diff --git a/.github/actions/prepare-native-runtime/action.yml b/.github/actions/prepare-native-runtime/action.yml index 61c289c052b..fe60ef1f656 100644 --- a/.github/actions/prepare-native-runtime/action.yml +++ b/.github/actions/prepare-native-runtime/action.yml @@ -54,7 +54,7 @@ runs: env: NATIVE_RUNTIME: ${{ inputs.native-runtime }} NODE_VERSION: ${{ inputs.node-version }} - NATIVE_SOURCE_HASH: ${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc', 'config/pnpmfile.cjs', '.github/actions/prepare-native-runtime/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/initialize-windows-native-toolchain.mjs', 'config/scripts/capture-windows-native-environment.cmd', 'config/scripts/rebuild-native-deps.mjs', 'config/scripts/node-pty-job-ownership.cjs', 'config/scripts/windows-pe-machine.cjs', 'config/scripts/windows-process-tree-gyp-rebuild.mjs', 'config/scripts/windows-process-tree-creation-time.cjs', 'config/scripts/install-electron-package-binary.mjs', 'config/scripts/electron-platform-path.mjs', 'config/scripts/zip-extractor-command.mjs', 'src/shared/zip-extractor-command.ts', 'config/scripts/shared-electron-dist-cache.mjs', 'config/scripts/space-sharing-copy.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json', 'native/windows-registry/index.js', 'native/windows-security/src/addon.cc', 'native/windows-security/binding.gyp', 'native/windows-security/package.json', 'native/windows-security/index.js') }} + NATIVE_SOURCE_HASH: ${{ hashFiles('.npmrc', 'config/pnpmfile.cjs', '.github/actions/prepare-native-runtime/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/initialize-windows-native-toolchain.mjs', 'config/scripts/capture-windows-native-environment.cmd', 'config/scripts/rebuild-native-deps.mjs', 'config/scripts/node-pty-job-ownership.cjs', 'config/scripts/windows-pe-machine.cjs', 'config/scripts/windows-process-tree-gyp-rebuild.mjs', 'config/scripts/windows-process-tree-creation-time.cjs', 'config/scripts/install-electron-package-binary.mjs', 'config/scripts/electron-platform-path.mjs', 'config/scripts/zip-extractor-command.mjs', 'src/shared/zip-extractor-command.ts', 'config/scripts/shared-electron-dist-cache.mjs', 'config/scripts/space-sharing-copy.mjs', 'config/scripts/shared-tree-publish.mjs', 'config/scripts/native-build-entry.cjs', 'config/scripts/native-build-entry-probe.cjs', 'config/scripts/shared-native-build.mjs', 'config/scripts/orcad-prebuild-node-gyp.mjs', 'config/scripts/msbuild-file-tracking.mjs', 'config/scripts/native-dependency-lock-digest.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json', 'native/windows-registry/index.js', 'native/windows-security/src/addon.cc', 'native/windows-security/binding.gyp', 'native/windows-security/package.json', 'native/windows-security/index.js') }} run: | scope="${ImageOS:-$RUNNER_OS}" if [ -r /etc/os-release ]; then @@ -63,8 +63,11 @@ runs: fi pnpm_version="$(pnpm --version)" test -n "$pnpm_version" + # Locked native closures and the Electron release, not the whole lockfile. + lock_digest="$(node config/scripts/native-dependency-lock-digest.mjs)" + test -n "$lock_digest" printf 'scope=%s\n' "$scope" >> "$GITHUB_OUTPUT" - printf 'key=native-modules-%s-%s-%s-%s-node%s-pnpm%s-%s\n' "$RUNNER_OS" "$scope" "$RUNNER_ARCH" "$NATIVE_RUNTIME" "$NODE_VERSION" "$pnpm_version" "$NATIVE_SOURCE_HASH" >> "$GITHUB_OUTPUT" + printf 'key=native-modules-%s-%s-%s-%s-node%s-pnpm%s-%s-%s\n' "$RUNNER_OS" "$scope" "$RUNNER_ARCH" "$NATIVE_RUNTIME" "$NODE_VERSION" "$pnpm_version" "$NATIVE_SOURCE_HASH" "$lock_digest" >> "$GITHUB_OUTPUT" cat >> "$GITHUB_OUTPUT" <<'ORCA_NATIVE_CACHE_PATHS' path<- + inputs.native-runtime != 'none' && inputs.persist-native-cache != 'false' && + !(github.event_name == 'pull_request' && runner.os == 'Windows') uses: actions/cache@v5 with: # Post-job saving cannot resolve nested composite step outputs. @@ -89,7 +95,9 @@ runs: - name: Restore compiled native modules without saving id: native-cache-restore-only - if: inputs.native-runtime != 'none' && inputs.persist-native-cache == 'false' + if: >- + inputs.native-runtime != 'none' && (inputs.persist-native-cache == 'false' || + github.event_name == 'pull_request' && runner.os == 'Windows') uses: actions/cache/restore@v5 with: path: | diff --git a/.github/actions/prepare-windows-agent-hook/action.yml b/.github/actions/prepare-windows-agent-hook/action.yml index b8c8a2ac7c5..d06251bfa1d 100644 --- a/.github/actions/prepare-windows-agent-hook/action.yml +++ b/.github/actions/prepare-windows-agent-hook/action.yml @@ -4,6 +4,14 @@ description: Compiles native/windows-agent-hook into native/windows-agent-hook/. runs: using: composite steps: + - name: Resolve the Windows Rust build scope + id: rust-scope + if: runner.os == 'Windows' + shell: bash + run: node config/scripts/windows-rust-build-scope.mjs >> "$GITHUB_OUTPUT" + + # Source globs, not crate/**: target/ and .build/ can already exist when this key is computed. + # Real inputs only: older restores are safe because the build re-checks the exe's fingerprint. - name: Cache the Windows agent hook build if: runner.os == 'Windows' uses: actions/cache@v5 @@ -12,16 +20,13 @@ runs: native/windows-agent-hook/.build native/windows-agent-hook/target ~/.cargo/registry - key: windows-agent-hook-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/windows-agent-hook/**', 'config/scripts/build-windows-agent-hook.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'resources/build/icon.ico', 'package.json') }} + key: windows-agent-hook-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust-scope.outputs.toolchain }}-${{ steps.rust-scope.outputs.app-version }}-${{ hashFiles('native/windows-agent-hook/src/**', 'native/windows-agent-hook/build.rs', 'native/windows-agent-hook/Cargo.toml', 'native/windows-agent-hook/Cargo.lock', 'native/windows-agent-hook/app.manifest', 'native/windows-agent-hook/.cargo/**', 'config/scripts/build-windows-agent-hook.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'config/scripts/windows-pe-imports.mjs', 'resources/build/icon.ico') }} + restore-keys: | + windows-agent-hook-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust-scope.outputs.toolchain }}- - # Why explicit: a runner image without cargo would otherwise fail inside electron-builder's verify. - name: Build the Windows agent hook if: runner.os == 'Windows' shell: pwsh run: | - if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { - rustup toolchain install stable --profile minimal - rustup default stable - } node config/scripts/build-windows-agent-hook.mjs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/.github/actions/prepare-windows-cli-launcher/action.yml b/.github/actions/prepare-windows-cli-launcher/action.yml index 4ac647972fa..5a18dc57a49 100644 --- a/.github/actions/prepare-windows-cli-launcher/action.yml +++ b/.github/actions/prepare-windows-cli-launcher/action.yml @@ -9,6 +9,14 @@ inputs: runs: using: composite steps: + - name: Resolve the Windows Rust build scope + id: rust-scope + if: runner.os == 'Windows' + shell: bash + run: node config/scripts/windows-rust-build-scope.mjs >> "$GITHUB_OUTPUT" + + # Source globs, not crate/**: target/ and .build/ can already exist when this key is computed. + # Real inputs only: older restores are safe because the build re-checks each exe's fingerprint. - name: Cache the Windows CLI launcher builds if: runner.os == 'Windows' uses: actions/cache@v5 @@ -17,19 +25,16 @@ runs: .build/windows-cli-launcher native/windows-cli-launcher/target ~/.cargo/registry - key: orcad-windows-cli-launcher-${{ runner.arch }}-${{ inputs.arches }}-${{ hashFiles('native/windows-cli-launcher/**', 'config/scripts/build-windows-cli-launcher.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'resources/build/icon.ico', 'package.json') }} + key: orcad-windows-cli-launcher-v2-${{ runner.arch }}-${{ inputs.arches }}-${{ steps.rust-scope.outputs.toolchain }}-${{ steps.rust-scope.outputs.app-version }}-${{ hashFiles('native/windows-cli-launcher/src/**', 'native/windows-cli-launcher/build.rs', 'native/windows-cli-launcher/Cargo.toml', 'native/windows-cli-launcher/Cargo.lock', 'native/windows-cli-launcher/app.manifest', 'native/windows-cli-launcher/.cargo/**', 'config/scripts/build-windows-cli-launcher.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'config/scripts/windows-pe-imports.mjs', 'resources/build/icon.ico') }} + restore-keys: | + orcad-windows-cli-launcher-v2-${{ runner.arch }}-${{ inputs.arches }}-${{ steps.rust-scope.outputs.toolchain }}- - # Why explicit: a runner image without cargo would otherwise fail inside the orcad build. - name: Build the launcher for each architecture if: runner.os == 'Windows' shell: pwsh env: ARCHES: ${{ inputs.arches }} run: | - if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { - rustup toolchain install stable --profile minimal - rustup default stable - } $triples = @{ x64 = 'x86_64-pc-windows-msvc'; arm64 = 'aarch64-pc-windows-msvc' } foreach ($arch in ($env:ARCHES -split ' ' | Where-Object { $_ })) { rustup target add $triples[$arch] diff --git a/.github/workflows/ci-cache-warmup-platform-jobs.yml b/.github/workflows/ci-cache-warmup-platform-jobs.yml index 780772f699d..7b24b8f6be8 100644 --- a/.github/workflows/ci-cache-warmup-platform-jobs.yml +++ b/.github/workflows/ci-cache-warmup-platform-jobs.yml @@ -95,6 +95,12 @@ jobs: - name: Verify native cache is usable run: node config/scripts/ensure-native-runtime.mjs --check-only + - name: Resolve the Windows Rust build scope + id: rust-scope + if: runner.arch == 'X64' && matrix.os != 'windows-2025' + shell: bash + run: node config/scripts/windows-rust-build-scope.mjs >> "$GITHUB_OUTPUT" + - name: Cache Windows CLI launcher if: runner.arch == 'X64' && matrix.os != 'windows-2025' uses: actions/cache@v5 @@ -105,16 +111,14 @@ jobs: native/windows-agent-hook/.build native/windows-agent-hook/target ~/.cargo/registry - key: windows-cli-launcher-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/windows-cli-launcher/**', 'native/windows-agent-hook/**', 'config/scripts/build-windows-cli-launcher.mjs', 'config/scripts/build-windows-agent-hook.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'resources/build/icon.ico', 'package.json') }} + key: windows-cli-launcher-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust-scope.outputs.toolchain }}-${{ steps.rust-scope.outputs.app-version }}-${{ hashFiles('native/windows-cli-launcher/src/**', 'native/windows-cli-launcher/build.rs', 'native/windows-cli-launcher/Cargo.toml', 'native/windows-cli-launcher/Cargo.lock', 'native/windows-cli-launcher/app.manifest', 'native/windows-cli-launcher/.cargo/**', 'native/windows-agent-hook/src/**', 'native/windows-agent-hook/build.rs', 'native/windows-agent-hook/Cargo.toml', 'native/windows-agent-hook/Cargo.lock', 'native/windows-agent-hook/app.manifest', 'native/windows-agent-hook/.cargo/**', 'config/scripts/build-windows-cli-launcher.mjs', 'config/scripts/build-windows-agent-hook.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'config/scripts/windows-pe-imports.mjs', 'resources/build/icon.ico') }} + restore-keys: | + windows-cli-launcher-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust-scope.outputs.toolchain }}- - name: Build the Windows native executables if: runner.arch == 'X64' && matrix.os != 'windows-2025' shell: pwsh run: | - if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { - rustup toolchain install stable --profile minimal - rustup default stable - } node config/scripts/build-windows-agent-hook.mjs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } node config/scripts/build-windows-cli-launcher.mjs @@ -126,6 +130,56 @@ jobs: with: arches: arm64 + # node-server, Windows E2E and the survival lanes restore these exact keys. + - name: Warm the Windows x64 server launcher + if: matrix.os == 'windows-2022' + uses: ./.github/actions/prepare-windows-cli-launcher + with: + arches: x64 + - name: Warm the Windows agent hook + if: matrix.os == 'windows-2022' + uses: ./.github/actions/prepare-windows-agent-hook + + - name: Warm the Windows server prebuild + id: orcad-prebuild + if: matrix.os != 'windows-2025' + uses: ./.github/actions/prepare-orcad-prebuilds + with: + resolve-windows-cache: 'true' + restore-windows-cache: 'true' + # Same check a consumer runs before trusting a restored slot. + - name: Validate the Windows server prebuild before saving + id: orcad-prebuild-validate + if: >- + github.event_name != 'pull_request' && matrix.os != 'windows-2025' && + steps.orcad-prebuild.outputs.cache-hit != 'true' && steps.orcad-prebuild.outputs.cache-key != '' + run: node config/scripts/orcad-windows-prebuild-cache.mjs --validate + - name: Save the Windows server prebuild + if: steps.orcad-prebuild-validate.outcome == 'success' + continue-on-error: true + uses: actions/cache/save@v5 + with: + path: ${{ steps.orcad-prebuild.outputs.cache-path }} + key: ${{ steps.orcad-prebuild.outputs.cache-key }} + + # Windows packaging and E2E restore these; PRs no longer save their own copies. + warm-windows-electron: + concurrency: + group: ci-cache-warmup-windows-electron-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name != 'schedule' }} + runs-on: windows-2022 + timeout-minutes: 20 + env: + ORCA_BACKGROUND_LAUNCH: '1' + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: ./.github/actions/install-node-dependencies + with: + native-runtime: electron + cache-pnpm-store: 'false' + warm-linux-package-fixtures: if: github.event_name != 'pull_request' concurrency: diff --git a/.github/workflows/ci-cache-warmup-platforms.yml b/.github/workflows/ci-cache-warmup-platforms.yml index 35a667ef6aa..440a0976af8 100644 --- a/.github/workflows/ci-cache-warmup-platforms.yml +++ b/.github/workflows/ci-cache-warmup-platforms.yml @@ -11,6 +11,24 @@ on: - '.github/actions/restore-pnpm-verification/**' - '.github/actions/prepare-native-runtime/**' - '.github/actions/prepare-windows-cli-launcher/**' + - '.github/actions/prepare-windows-agent-hook/**' + - 'config/scripts/windows-rust-build-scope.mjs' + - '.github/actions/prepare-orcad-prebuilds/**' + - 'config/scripts/native-dependency-lock-digest.mjs' + - 'config/scripts/orcad-windows-prebuild-cache.mjs' + - 'config/scripts/shared-native-build.mjs' + - 'config/scripts/shared-tree-publish.mjs' + - 'config/scripts/native-build-entry*.cjs' + - 'config/scripts/orcad-prebuild-node-gyp.mjs' + - 'config/scripts/msbuild-file-tracking.mjs' + - 'config/scripts/windows-pe-imports.mjs' + - 'config/scripts/build-orcad-prebuilds.mjs' + - 'config/scripts/orcad-prebuild-slot-contents.mjs' + - 'config/scripts/pinned-node-downloads.mjs' + - 'config/scripts/update-node-runtime-pin.mjs' + - 'config/scripts/build-workspace-packages.mjs' + - 'src/shared/node-runtime-pin.ts' + - 'src/packages/process-host/**' - '.github/actions/prepare-git-compatibility/**' - '.github/actions/prepare-linux-package-fixture/**' - 'config/docker/headless-serve-shutdown/**' diff --git a/.github/workflows/dev-channel-win-build.yml b/.github/workflows/dev-channel-win-build.yml index 1efa6aa8adc..a7ae8202980 100644 --- a/.github/workflows/dev-channel-win-build.yml +++ b/.github/workflows/dev-channel-win-build.yml @@ -195,6 +195,12 @@ jobs: pnpm-lock.yaml mobile/pnpm-lock.yaml + # Electron and builder releases fix what this downloads; the lockfile churns far more often. + - name: Resolve electron-builder download versions + id: eb-versions + shell: bash + run: node -p "const d = require('./package.json').devDependencies; 'versions=electron' + d.electron + '-builder' + d['electron-builder']" >> "$GITHUB_OUTPUT" + # Cache Electron/NSIS/winCodeSign tools with release-cut’s key shape. - name: Cache electron-builder downloads uses: actions/cache@v5 @@ -202,7 +208,7 @@ jobs: path: | ~\AppData\Local\electron\Cache ~\AppData\Local\electron-builder\Cache - key: electron-builder-win-${{ hashFiles('pnpm-lock.yaml') }} + key: electron-builder-win-${{ steps.eb-versions.outputs.versions }} restore-keys: | electron-builder-win- diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index 2ce7c6eba81..0d0237a102d 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -192,6 +192,7 @@ jobs: uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true - name: Test if: steps.test-scope.outputs.should_run != 'false' @@ -321,6 +322,7 @@ jobs: uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true - name: Test env: diff --git a/.github/workflows/node-server-tests.yml b/.github/workflows/node-server-tests.yml index 9f8623dcefb..cab510df828 100644 --- a/.github/workflows/node-server-tests.yml +++ b/.github/workflows/node-server-tests.yml @@ -187,6 +187,7 @@ jobs: if: runner.os == 'Linux' with: bun-version-file: config/.bun-version + no-cache: true - name: Build the last Bun orcad for the cross-runtime tests id: bun-orcad background: true diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 0d35e670e10..5d74177b46f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -101,6 +101,7 @@ jobs: uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true - name: Check required PR context ownership run: bun .github/scripts/check-required-pr-context.mjs @@ -220,6 +221,7 @@ jobs: uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true # Plan first: its ~10s single-threaded import-graph walk overlaps lint instead of trailing the job. - name: Plan unit selection @@ -1270,6 +1272,12 @@ jobs: with: persist-credentials: false + # Electron and builder releases fix what this downloads; the lockfile churns far more often. + - name: Resolve electron-builder download versions + id: eb-versions + shell: bash + run: node -p "const d = require('./package.json').devDependencies; 'versions=electron' + d.electron + '-builder' + d['electron-builder']" >> "$GITHUB_OUTPUT" + - name: Cache electron-builder downloads uses: actions/cache/restore@v5 with: @@ -1277,7 +1285,7 @@ jobs: ~\AppData\Local\electron\Cache ~\AppData\Local\electron-builder\Cache # Release builds seed this exact path set; PR-local copies cannot serve other PRs. - key: electron-builder-win-${{ hashFiles('pnpm-lock.yaml') }} + key: electron-builder-win-${{ steps.eb-versions.outputs.versions }} restore-keys: | electron-builder-win- @@ -1295,13 +1303,6 @@ jobs: # build resolves React Native and Expo from mobile/node_modules. - uses: ./.github/actions/install-mobile-dependencies - - name: Save compiled Node native modules - if: steps.deps.outputs.native-cache-hit != 'true' - uses: actions/cache/save@v5 - with: - path: ${{ steps.deps.outputs.native-cache-path }} - key: ${{ steps.deps.outputs.native-cache-key }} - # vitest runs here directly rather than through `pnpm test`, so the addon # assertions only hold once install-node-dependencies has rebuilt natives. - name: Test Windows installer process probe @@ -1315,6 +1316,12 @@ jobs: config/scripts/nsis-process-check.test.mjs config/scripts/idle-cpu-process-sampling.test.mjs + - name: Resolve the Windows Rust build scope + id: rust-scope + shell: bash + run: node config/scripts/windows-rust-build-scope.mjs >> "$GITHUB_OUTPUT" + + # Real inputs only: older restores are safe because the build re-checks each exe's fingerprint. - name: Cache Windows CLI launcher uses: actions/cache@v5 with: @@ -1325,7 +1332,9 @@ jobs: native/windows-agent-hook/.build native/windows-agent-hook/target ~/.cargo/registry - key: windows-cli-launcher-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/windows-cli-launcher/**', 'native/windows-agent-hook/**', 'config/scripts/build-windows-cli-launcher.mjs', 'config/scripts/build-windows-agent-hook.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'resources/build/icon.ico', 'package.json') }} + key: windows-cli-launcher-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust-scope.outputs.toolchain }}-${{ steps.rust-scope.outputs.app-version }}-${{ hashFiles('native/windows-cli-launcher/src/**', 'native/windows-cli-launcher/build.rs', 'native/windows-cli-launcher/Cargo.toml', 'native/windows-cli-launcher/Cargo.lock', 'native/windows-cli-launcher/app.manifest', 'native/windows-cli-launcher/.cargo/**', 'native/windows-agent-hook/src/**', 'native/windows-agent-hook/build.rs', 'native/windows-agent-hook/Cargo.toml', 'native/windows-agent-hook/Cargo.lock', 'native/windows-agent-hook/app.manifest', 'native/windows-agent-hook/.cargo/**', 'config/scripts/build-windows-cli-launcher.mjs', 'config/scripts/build-windows-agent-hook.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'config/scripts/windows-pe-imports.mjs', 'resources/build/icon.ico') }} + restore-keys: | + windows-cli-launcher-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust-scope.outputs.toolchain }}- # Require cargo early; otherwise packaged Rust launcher fails deep inside builder native hooks. - name: Ensure the Rust toolchain for the Windows CLI launcher diff --git a/.github/workflows/preflight-benchmark.yml b/.github/workflows/preflight-benchmark.yml index ac038c8f260..be9db927a41 100644 --- a/.github/workflows/preflight-benchmark.yml +++ b/.github/workflows/preflight-benchmark.yml @@ -31,6 +31,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true - name: Compare native and Bun typechecking run: node config/scripts/benchmark-preflight-compilers.mjs - uses: actions/upload-artifact@v7 @@ -54,6 +55,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true - name: Compare anti-slop patch and script runtimes run: node config/scripts/benchmark-preflight-static-analysis.mjs - uses: actions/upload-artifact@v7 @@ -78,6 +80,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true - name: Compare compatibility-import scan and runtimes run: node config/scripts/benchmark-preflight-compat-boundary.mjs - uses: actions/upload-artifact@v7 @@ -101,6 +104,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true - name: Compare localization extraction runtimes and complete catalogs run: node config/scripts/benchmark-preflight-localization-extraction.mjs - uses: actions/upload-artifact@v7 diff --git a/.github/workflows/preflight-critical-path-benchmark.yml b/.github/workflows/preflight-critical-path-benchmark.yml index c09182ae802..c6e8eb44b4a 100644 --- a/.github/workflows/preflight-critical-path-benchmark.yml +++ b/.github/workflows/preflight-critical-path-benchmark.yml @@ -34,6 +34,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true - uses: ./.github/actions/install-mobile-dependencies if: matrix.comparison == 'smoke' - name: Compare checks and verify their results diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index 7ae1f1d613e..47ddd30176f 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -1196,12 +1196,18 @@ jobs: npm install -g node-gyp@11.5.0 echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV" + # Electron and builder releases fix what this downloads; the lockfile churns far more often. + - name: Resolve electron-builder download versions + id: eb-versions + shell: bash + run: node -p "const d = require('./package.json').devDependencies; 'versions=electron' + d.electron + '-builder' + d['electron-builder']" >> "$GITHUB_OUTPUT" + # Cache Electron and builder downloads (winCodeSign/NSIS/Squirrel/AppImage), saving ~30–90s per job. - name: Cache electron-builder downloads uses: actions/cache@v5 with: path: ${{ matrix.eb_cache_path }} - key: electron-builder-${{ matrix.platform }}-${{ hashFiles('pnpm-lock.yaml') }} + key: electron-builder-${{ matrix.platform }}-${{ steps.eb-versions.outputs.versions }} restore-keys: | electron-builder-${{ matrix.platform }}- diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 48272d15ba5..0bea124ed91 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -99,6 +99,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true - name: Resolve unit module cache id: module-cache diff --git a/.github/workflows/win-orcad-serve-switch-e2e.yml b/.github/workflows/win-orcad-serve-switch-e2e.yml index a6e0c5a58a8..2ece63d61e7 100644 --- a/.github/workflows/win-orcad-serve-switch-e2e.yml +++ b/.github/workflows/win-orcad-serve-switch-e2e.yml @@ -56,6 +56,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true # Before the template exists: packaging verifies a present template for every target. # Uncached: a src/**-keyed installer hit ~0.3% of runs while each 173 MB save evicted shared caches. diff --git a/.github/workflows/windows-signing-rehearsal.yml b/.github/workflows/windows-signing-rehearsal.yml index 15d5da2f8ac..86a553fc495 100644 --- a/.github/workflows/windows-signing-rehearsal.yml +++ b/.github/workflows/windows-signing-rehearsal.yml @@ -62,13 +62,19 @@ jobs: pnpm-lock.yaml mobile/pnpm-lock.yaml + # Electron and builder releases fix what this downloads; the lockfile churns far more often. + - name: Resolve electron-builder download versions + id: eb-versions + shell: bash + run: node -p "const d = require('./package.json').devDependencies; 'versions=electron' + d.electron + '-builder' + d['electron-builder']" >> "$GITHUB_OUTPUT" + - name: Cache electron-builder downloads uses: actions/cache@v5 with: path: | ~\AppData\Local\electron\Cache ~\AppData\Local\electron-builder\Cache - key: electron-builder-win-${{ hashFiles('pnpm-lock.yaml') }} + key: electron-builder-win-${{ steps.eb-versions.outputs.versions }} restore-keys: | electron-builder-win- @@ -484,7 +490,7 @@ jobs: # Only elevate.exe is advisory: NSIS pack recopies pristine cache bytes over signed win-unpacked output. # With no certificate, CopyElevateHelper.signIf signs nothing; this predates the uninstaller relay. # release-cut pre-seeds only Valid, SignPath-signed cache bytes; test certificates deliberately fail that guard. - # DO NOT relax it: rehearsal shares electron-builder-win- with real releases and would poison + # DO NOT relax it: rehearsal shares electron-builder-win- with real releases and would poison # their cache with “Test certificate for Orca agent ide [OSS]”. Fix the UAC helper separately with proper scrutiny. if ($relative -eq 'resources\elevate.exe') { Test-Signature "installed: $relative" $path -Advisory diff --git a/.github/workflows/windows-typecheck-parity.yml b/.github/workflows/windows-typecheck-parity.yml index 4e69c84afe5..c007455460c 100644 --- a/.github/workflows/windows-typecheck-parity.yml +++ b/.github/workflows/windows-typecheck-parity.yml @@ -43,6 +43,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true - name: Compare compiler diagnostics and project file lists run: node config/scripts/benchmark-preflight-compilers.mjs --parity-only - uses: actions/upload-artifact@v7 diff --git a/.github/workflows/zcode-credential-contract.yml b/.github/workflows/zcode-credential-contract.yml index b9688b1bf9e..9e66cef141e 100644 --- a/.github/workflows/zcode-credential-contract.yml +++ b/.github/workflows/zcode-credential-contract.yml @@ -87,6 +87,7 @@ jobs: id: bun with: bun-version-file: config/.bun-version + no-cache: true - name: Verify read-only credentials and account changes if: ${{ !cancelled() && steps.scope.outputs.account == 'true' && steps.dependencies.outcome == 'success' && steps.bun.outcome == 'success' }} run: pnpm test src/main/zcode/zcode-v2-plan-credentials.test.ts src/main/zcode/zcode-v2-personal-overlay-raw-json.test.ts src/main/rate-limits/service-zcode-usage.test.ts src/shared/node-bounded-file-reader-sync.test.ts diff --git a/.github/workflows/zcode-transcript-contract.yml b/.github/workflows/zcode-transcript-contract.yml index 4c27c29ab73..b9c19f9c702 100644 --- a/.github/workflows/zcode-transcript-contract.yml +++ b/.github/workflows/zcode-transcript-contract.yml @@ -38,5 +38,6 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version + no-cache: true - name: Verify SQLite ordering, legacy readers, and host routing run: pnpm test src/main/native-chat/transcript-zcode.test.ts src/main/native-chat/transcript-opencode.test.ts src/main/native-chat/transcript-opencode-sqlite-query.test.ts src/main/native-chat/transcript-opencode2-sqlite.test.ts src/main/native-chat/transcript-opencode-semantic-fidelity.test.ts src/main/native-chat/transcript-opencode-read-bounds.test.ts src/main/native-chat/transcript-opencode-subscribe.test.ts src/main/ai-vault/session-scanner-zcode.test.ts src/main/ai-vault/session-scanner-zcode-sources.test.ts src/main/ai-vault/opencode-wsl-runtime-preparation.test.ts src/main/ai-vault/session-scanner-opencode-wsl-routing.test.ts src/shared/native-chat-transcript-projection.test.ts src/renderer/src/components/native-chat/structured-agent-session-transcript-order.test.ts diff --git a/config/scripts/ci-dependency-download-cache.test.mjs b/config/scripts/ci-dependency-download-cache.test.mjs index f391b2af222..14427ddf15f 100644 --- a/config/scripts/ci-dependency-download-cache.test.mjs +++ b/config/scripts/ci-dependency-download-cache.test.mjs @@ -522,8 +522,8 @@ it('shares Electron archives with PRs without uploading PR-local copies', () => const restore = action.runs.steps.find( (step) => step.name === 'Restore Electron package archive without saving' ) - expect(save.if).toContain("github.event_name != 'pull_request' || runner.os != 'Linux'") - expect(restore.if).toContain("github.event_name == 'pull_request' && runner.os == 'Linux'") + expect(save.if).toContain("github.event_name != 'pull_request' &&") + expect(restore.if).toContain("github.event_name == 'pull_request' &&") expect(save.if).toContain("steps.electron-package-cache.outputs.version != ''") expect(restore.if).toContain("steps.electron-package-cache.outputs.version != ''") expect(save.uses).toBe('actions/cache@v5') diff --git a/config/scripts/ci-native-cache.test.mjs b/config/scripts/ci-native-cache.test.mjs index 9671787fa1c..9045fe926ea 100644 --- a/config/scripts/ci-native-cache.test.mjs +++ b/config/scripts/ci-native-cache.test.mjs @@ -28,7 +28,8 @@ function resolveIdentity({ arch = 'X64', image = 'win22', libc = null, - sourceDigest = 'a'.repeat(64) + sourceDigest = 'a'.repeat(64), + lockDigest = 'c'.repeat(64) } = {}) { const directory = mkdtempSync(join(tmpdir(), 'orca-native-identity-')) try { @@ -38,7 +39,7 @@ function resolveIdentity({ writeFileSync(osRelease, `ID=${libc.id}\nVERSION_ID=${libc.version}\n`) } // Isolate the container file lookup while running the production identity commands. - const script = `pnpm() { printf "%s\\n" "$TEST_PNPM_VERSION"; }\n${identity.run.replaceAll('/etc/os-release', '"$TEST_OS_RELEASE"')}` + const script = `pnpm() { printf "%s\\n" "$TEST_PNPM_VERSION"; }\nnode() { printf "%s\\n" "$TEST_LOCK_DIGEST"; }\n${identity.run.replaceAll('/etc/os-release', '"$TEST_OS_RELEASE"')}` const execution = runProcessSync({ program: 'bash', args: ['-e', '-o', 'pipefail', '-c', script], @@ -51,6 +52,7 @@ function resolveIdentity({ ImageOS: image, NATIVE_SOURCE_HASH: sourceDigest, TEST_PNPM_VERSION: pnpm, + TEST_LOCK_DIGEST: lockDigest, TEST_OS_RELEASE: osRelease, GITHUB_OUTPUT: output } @@ -84,6 +86,7 @@ describe('CI native cache ownership', () => { expect(baseline.path.split('\n')).toEqual(paths) const variants = [ { sourceDigest: 'b'.repeat(64) }, + { lockDigest: 'd'.repeat(64) }, { runtime: 'electron' }, { node: 'v24.22.0' }, { pnpm: '12.1.0' }, @@ -134,7 +137,7 @@ describe('CI native cache ownership', () => { } ) - it('uses requested identity on both restores and saves the Node ABI before Windows switches to Electron', () => { + it('uses requested identity on every restore and leaves Windows PR saves to the main warmers', () => { for (const step of native.runs.steps.filter((step) => step.uses?.startsWith('actions/cache'))) { expect(step.with.key).toBe('${{ steps.native-cache-scope.outputs.key }}') expect(step.with.path.trim().split('\n')).toEqual(paths) @@ -146,11 +149,12 @@ describe('CI native cache ownership', () => { const build = job.steps.find((step) => step.name === 'Build package inputs') const electron = job.steps.find((step) => step.name === 'Prepare Electron native runtime') expect(install.with['persist-native-cache']).toBe('false') - expect(save.with).toEqual({ - key: '${{ steps.deps.outputs.native-cache-key }}', - path: '${{ steps.deps.outputs.native-cache-path }}' - }) - expect(job.steps.indexOf(save)).toBeLessThan(job.steps.indexOf(build)) + expect(save).toBeUndefined() + const saving = native.runs.steps.find((step) => step.id === 'native-cache-restore') + expect(saving.if).toContain("!(github.event_name == 'pull_request' && runner.os == 'Windows')") + const warm = parse(readFileSync('.github/workflows/ci-cache-warmup-platform-jobs.yml', 'utf8')) + expect(warm.jobs['warm-windows'].strategy.matrix.os).toContain(job['runs-on']) + expect(warm.jobs['warm-windows-electron']['runs-on']).toBe(job['runs-on']) expect(job.steps.indexOf(build)).toBeLessThan(job.steps.indexOf(electron)) expect(electron.uses).toBe('./.github/actions/prepare-native-runtime') expect(electron.with).toEqual({ diff --git a/config/scripts/native-dependency-lock-digest.mjs b/config/scripts/native-dependency-lock-digest.mjs new file mode 100644 index 00000000000..102996ebd5b --- /dev/null +++ b/config/scripts/native-dependency-lock-digest.mjs @@ -0,0 +1,147 @@ +import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { parse, parseAllDocuments } from 'yaml' + +// Packages whose locked dependency closure is compiled into, or compiles, the cached addons. +export const NATIVE_MODULE_BUILD_PACKAGES = [ + 'node-pty', + '@vscode/windows-process-tree', + '@electron/rebuild', + 'node-gyp' +] +// Only the release matters: it fixes the ABI and headers; its downloader closure does not. +export const NATIVE_MODULE_TARGET_PACKAGES = ['electron'] +export const NATIVE_MODULE_WORKSPACES = ['native/windows-registry', 'native/windows-security'] +// Resolution-only settings, plus patches, whose native effect already shows in snapshot ids. +const RESOLUTION_ONLY_WORKSPACE_SETTINGS = new Set([ + 'addMissingPeerTypes', + 'catalog', + 'catalogs', + 'ignoredOptionalDependencies', + 'minimumReleaseAge', + 'minimumReleaseAgeExclude', + 'overrides', + 'packageExtensions', + 'patchedDependencies' +]) + +/** `@scope/name@1.0.0(peer@2.0.0)` -> `@scope/name`. */ +function snapshotName(key) { + const bare = key.replace(/\(.*$/, '') + return bare.slice(0, bare.lastIndexOf('@')) +} + +function versionOf(entry) { + return typeof entry === 'string' ? entry : entry?.version +} + +/** pnpm 12 splits the lockfile into a package-manager document and the project one; merge both. */ +function readLockfile(contents) { + const importers = {} + const snapshots = {} + for (const document of parseAllDocuments(contents)) { + if (document.errors.length) { + throw document.errors[0] + } + const lockfile = document.toJS() ?? {} + for (const [path, importer] of Object.entries(lockfile.importers ?? {})) { + importers[path] = { ...importers[path], ...importer } + } + Object.assign(snapshots, lockfile.snapshots) + } + return { importers, snapshots } +} + +/** Sorted snapshot ids (versions and patch hashes) a set of native packages build from. */ +export function nativeDependencyClosure( + contents, + { packages, versionOnly = [], workspaces = [] } = {} +) { + const { importers, snapshots } = readLockfile(contents) + const names = Object.keys(snapshots).map(snapshotName) + const unlocked = [...packages, ...versionOnly].filter((name) => !names.includes(name)) + if (unlocked.length > 0) { + // Fail closed: a package that silently stopped matching would drop out of the key. + throw new Error(`pnpm-lock.yaml locks none of ${unlocked.join(', ')}`) + } + const pending = Object.keys(snapshots).filter((key) => packages.includes(snapshotName(key))) + for (const workspace of workspaces) { + const importer = importers[workspace] + if (!importer) { + throw new Error(`pnpm-lock.yaml has no importer for ${workspace}`) + } + for (const field of ['dependencies', 'optionalDependencies', 'devDependencies']) { + for (const [name, entry] of Object.entries(importer[field] ?? {})) { + const version = versionOf(entry) + if (!version.startsWith('link:')) { + pending.push(`${name}@${version}`) + } + } + } + } + const visited = new Set() + while (pending.length > 0) { + const key = pending.pop() + if (visited.has(key)) { + continue + } + const snapshot = snapshots[key] + if (!snapshot) { + throw new Error(`pnpm-lock.yaml has no snapshot for ${key}`) + } + visited.add(key) + for (const field of ['dependencies', 'optionalDependencies']) { + for (const [name, version] of Object.entries(snapshot[field] ?? {})) { + pending.push(`${name}@${version}`) + } + } + } + for (const key of Object.keys(snapshots)) { + if (versionOnly.includes(snapshotName(key))) { + visited.add(key.replace(/\(.*$/, '')) + } + } + return [...visited].sort() +} + +/** pnpm-workspace.yaml minus resolution-only settings, with build approvals for `closure` only. */ +export function nativeWorkspaceSettings(contents, closure) { + const names = new Set(closure.map(snapshotName)) + return Object.fromEntries( + Object.entries(parse(contents) ?? {}) + .filter(([setting]) => !RESOLUTION_ONLY_WORKSPACE_SETTINGS.has(setting)) + .map(([setting, value]) => + setting === 'allowBuilds' && value && typeof value === 'object' + ? [setting, Object.fromEntries(Object.entries(value).filter(([name]) => names.has(name)))] + : [setting, value] + ) + .sort(([left], [right]) => left.localeCompare(right)) + ) +} + +export function nativeDependencyLockDigest({ + repository = resolve(import.meta.dirname, '../..'), + ...selection +}) { + const closure = nativeDependencyClosure( + readFileSync(join(repository, 'pnpm-lock.yaml'), 'utf8'), + selection + ) + const settings = nativeWorkspaceSettings( + readFileSync(join(repository, 'pnpm-workspace.yaml'), 'utf8'), + closure + ) + const digest = createHash('sha256').update(JSON.stringify({ closure, settings })).digest('hex') + return { digest, closure } +} + +if (process.argv[1]?.endsWith('native-dependency-lock-digest.mjs')) { + const { digest, closure } = nativeDependencyLockDigest({ + packages: NATIVE_MODULE_BUILD_PACKAGES, + versionOnly: NATIVE_MODULE_TARGET_PACKAGES, + workspaces: NATIVE_MODULE_WORKSPACES + }) + console.error(`[native-cache] locked inputs: ${closure.join(' ')}`) + console.log(digest) +} diff --git a/config/scripts/native-dependency-lock-digest.test.mjs b/config/scripts/native-dependency-lock-digest.test.mjs new file mode 100644 index 00000000000..fbb00f024db --- /dev/null +++ b/config/scripts/native-dependency-lock-digest.test.mjs @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest' +import { + nativeDependencyClosure, + nativeWorkspaceSettings +} from './native-dependency-lock-digest.mjs' + +const lockfile = ({ + pty = 'node-pty@1.1.0(patch_hash=aa)', + addon = '7.1.1', + electron = '43.8.1' +}) => `--- +lockfileVersion: '9.0' +importers: + .: + packageManagerDependencies: + pnpm: {specifier: 12.14.0, version: 12.14.0} +--- +lockfileVersion: '9.0' +importers: + .: + devDependencies: + electron: {specifier: ${electron}, version: ${electron}(supports-color@7.2.0)} + native/addon: + dependencies: + node-addon-api: {specifier: ^8.5.0, version: 8.9.2} +snapshots: + ${pty}: + dependencies: + node-addon-api: ${addon} + node-addon-api@7.1.0: {} + node-addon-api@7.1.1: {} + node-addon-api@8.9.2: {} + electron@${electron}(supports-color@7.2.0): + dependencies: + got: 11.8.6 + got@11.8.6: {} + zustand@5.0.15: {} +` +const selection = { + packages: ['node-pty'], + versionOnly: ['electron'], + workspaces: ['native/addon'] +} + +describe('native dependency lock closure', () => { + it('pins native closures, workspace addons and the Electron release only', () => { + expect(nativeDependencyClosure(lockfile({}), selection)).toEqual([ + 'electron@43.8.1', + 'node-addon-api@7.1.1', + 'node-addon-api@8.9.2', + 'node-pty@1.1.0(patch_hash=aa)' + ]) + for (const change of [ + { pty: 'node-pty@1.1.0(patch_hash=bb)' }, + { addon: '7.1.0' }, + { electron: '43.9.0' } + ]) { + expect(nativeDependencyClosure(lockfile(change), selection)).not.toEqual( + nativeDependencyClosure(lockfile({}), selection) + ) + } + }) + + it('fails closed when a selected package or dependency is not locked', () => { + expect(() => nativeDependencyClosure(lockfile({}), { packages: ['missing'] })).toThrow( + /locks none of missing/ + ) + expect(() => + nativeDependencyClosure(lockfile({}), { ...selection, packages: ['node-pty', 'node-gyp'] }) + ).toThrow(/locks none of node-gyp$/) + expect(() => + nativeDependencyClosure(lockfile({}), { ...selection, versionOnly: ['electron', 'nw'] }) + ).toThrow(/locks none of nw$/) + expect(() => + nativeDependencyClosure(lockfile({ addon: '9.9.9' }), { packages: ['node-pty'] }) + ).toThrow(/no snapshot for node-addon-api@9.9.9/) + }) + + it('keeps layout settings and native build approvals but drops resolution-only settings', () => { + const settings = ` +shamefullyHoist: true +minimumReleaseAge: 4320 +overrides: {a: 1} +patchedDependencies: {x: y} +allowBuilds: {node-pty: true, bun: true} +` + expect(nativeWorkspaceSettings(settings, ['node-pty@1.1.0(patch_hash=aa)'])).toEqual({ + allowBuilds: { 'node-pty': true }, + shamefullyHoist: true + }) + }) +}) diff --git a/config/scripts/orcad-windows-prebuild-cache.mjs b/config/scripts/orcad-windows-prebuild-cache.mjs index 88f9cd5df0f..843102103ac 100644 --- a/config/scripts/orcad-windows-prebuild-cache.mjs +++ b/config/scripts/orcad-windows-prebuild-cache.mjs @@ -4,6 +4,7 @@ import { createRequire } from 'node:module' import { dirname, join, resolve } from 'node:path' import { NODE_RUNTIME_PIN } from '../../src/shared/node-runtime-pin.ts' import { assertNodePtyPatchApplied } from './build-orcad-prebuilds.mjs' +import { nativeDependencyLockDigest } from './native-dependency-lock-digest.mjs' import { findPostBaselineNodeApiNames, findSlotProblems, @@ -17,11 +18,11 @@ import { const require = createRequire(import.meta.url) const root = resolve(import.meta.dirname, '../..') const ownership = require('./node-pty-job-ownership.cjs') +// The locked node-pty and node-gyp closures stand in for pnpm-lock.yaml and package.json. +export const WINDOWS_PREBUILD_LOCKED_PACKAGES = ['node-pty', 'node-gyp'] +const WINDOWS_PREBUILD_PACKAGE_SCRIPTS = ['build:orcad-prebuilds', 'build:packages'] export const WINDOWS_PREBUILD_CACHE_INPUTS = [ - 'package.json', '.github/actions/prepare-orcad-prebuilds/action.yml', - 'pnpm-lock.yaml', - 'pnpm-workspace.yaml', 'config/patches/node-pty@1.1.0.patch', 'src/shared/node-runtime-pin.ts', 'config/scripts/build-orcad-prebuilds.mjs', @@ -30,6 +31,8 @@ export const WINDOWS_PREBUILD_CACHE_INPUTS = [ 'config/scripts/capture-windows-native-environment.cmd', 'config/scripts/orcad-prebuild-slot-contents.mjs', 'config/scripts/orcad-windows-prebuild-cache.mjs', + 'config/scripts/native-dependency-lock-digest.mjs', + 'config/scripts/build-workspace-packages.mjs', 'config/scripts/node-pty-job-ownership.cjs', 'config/scripts/windows-pe-machine.cjs', 'config/scripts/pinned-node-downloads.mjs', @@ -65,28 +68,32 @@ export function windowsPrebuildCacheIdentity({ repository = root, platform = process.platform, arch = process.arch, - imageOS = process.env.ImageOS, - imageVersion = process.env.ImageVersion + imageOS = process.env.ImageOS } = {}) { const slot = windowsSlot(platform, arch) - if (!imageOS || !imageVersion) { - throw new Error('Windows prebuild cache needs the hosted ImageOS and ImageVersion') + if (!imageOS) { + throw new Error('Windows prebuild cache needs the hosted ImageOS') } - const digest = createHash('sha256').update(JSON.stringify({ slot, imageOS, imageVersion })) + // ImageOS, not the weekly ImageVersion: --validate re-checks N-API imports, arch and patches on restore. + const { scripts } = JSON.parse(readFileSync(join(repository, 'package.json'), 'utf8')) + const locked = nativeDependencyLockDigest({ + repository, + packages: WINDOWS_PREBUILD_LOCKED_PACKAGES + }).digest + const digest = createHash('sha256').update( + JSON.stringify({ + slot, + imageOS, + locked, + scripts: WINDOWS_PREBUILD_PACKAGE_SCRIPTS.map((name) => [name, scripts?.[name] ?? null]) + }) + ) for (const file of WINDOWS_PREBUILD_CACHE_INPUTS) { - const bytes = readFileSync(join(repository, file)) - if (file === 'package.json') { - const manifest = JSON.parse(bytes.toString('utf8')) - // App versions do not affect the separately versioned node-pty binary. - delete manifest.version - digest.update(`\0${file}\0`).update(JSON.stringify(manifest)) - } else { - digest.update(`\0${file}\0`).update(bytes) - } + digest.update(`\0${file}\0`).update(readFileSync(join(repository, file))) } return { slot, - key: `orcad-windows-prebuild-v1-${slot}-${digest.digest('hex')}`, + key: `orcad-windows-prebuild-v2-${slot}-${digest.digest('hex')}`, path: join(repository, 'out', 'orcad-prebuilds') } } diff --git a/config/scripts/orcad-windows-prebuild-cache.test.mjs b/config/scripts/orcad-windows-prebuild-cache.test.mjs index 3f8dbbf7d37..0cfacc1d8a5 100644 --- a/config/scripts/orcad-windows-prebuild-cache.test.mjs +++ b/config/scripts/orcad-windows-prebuild-cache.test.mjs @@ -205,22 +205,16 @@ describe('Windows server prebuild cache validation', () => { }) describe('Windows server prebuild cache inputs', () => { - const options = { platform: 'win32', arch: 'x64', imageOS: 'win22', imageVersion: '20260927.1.0' } + const options = { platform: 'win32', arch: 'x64', imageOS: 'win22' } - it('separates host architecture and compiler image without fallback keys', () => { + it('separates host architecture and image without fallback keys', () => { const identity = windowsPrebuildCacheIdentity(options) - expect(identity.key).toMatch(/^orcad-windows-prebuild-v1-win32-x64-[a-f0-9]{64}$/) + expect(identity.key).toMatch(/^orcad-windows-prebuild-v2-win32-x64-[a-f0-9]{64}$/) expect(windowsPrebuildCacheIdentity(options)).toEqual(identity) - for (const override of [ - { arch: 'arm64' }, - { imageOS: 'win11' }, - { imageVersion: '20261001.1.0' } - ]) { + for (const override of [{ arch: 'arm64' }, { imageOS: 'win25' }]) { expect(windowsPrebuildCacheIdentity({ ...options, ...override }).key).not.toBe(identity.key) } - expect(() => windowsPrebuildCacheIdentity({ ...options, imageVersion: '' })).toThrow( - /ImageVersion/ - ) + expect(() => windowsPrebuildCacheIdentity({ ...options, imageOS: '' })).toThrow(/ImageOS/) }) const compilerInputs = [ @@ -246,42 +240,70 @@ describe('Windows server prebuild cache inputs', () => { ] const cacheInputs = [...new Set([...WINDOWS_PREBUILD_CACHE_INPUTS, ...compilerInputs])] - it('reuses native binaries across app versions but invalidates every other manifest field', () => { - const repository = mkdtempSync(join(tmpdir(), 'orca-prebuild-version-key-')) + function repositoryCopy() { + const repository = mkdtempSync(join(tmpdir(), 'orca-prebuild-key-')) temporary.push(repository) const source = resolve(import.meta.dirname, '../..') - for (const relative of cacheInputs) { + for (const relative of [ + ...cacheInputs, + 'package.json', + 'pnpm-lock.yaml', + 'pnpm-workspace.yaml' + ]) { mkdirSync(dirname(join(repository, relative)), { recursive: true }) cpSync(join(source, relative), join(repository, relative)) } - const path = join(repository, 'package.json') - const manifest = JSON.parse(readFileSync(path, 'utf8')) + return repository + } + + it('ignores app versions and unrelated manifest and lockfile edits but not the build scripts', () => { + const repository = repositoryCopy() + const key = () => windowsPrebuildCacheIdentity({ ...options, repository }).key + const before = key() + const manifestPath = join(repository, 'package.json') + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) + writeFileSync( + manifestPath, + JSON.stringify({ + ...manifest, + version: '999.0.0', + dependencies: { ...manifest.dependencies, unrelated: '1.0.0' } + }) + ) + const lockPath = join(repository, 'pnpm-lock.yaml') + writeFileSync(lockPath, `${readFileSync(lockPath, 'utf8')}\n unrelated-package@1.0.0: {}\n`) + expect(key()).toBe(before) + writeFileSync( + manifestPath, + JSON.stringify({ + ...manifest, + scripts: { ...manifest.scripts, 'build:orcad-prebuilds': 'changed' } + }) + ) + expect(key()).not.toBe(before) + }) + + it('invalidates a changed node-pty or node-gyp lock entry', () => { + const repository = repositoryCopy() const before = windowsPrebuildCacheIdentity({ ...options, repository }).key - writeFileSync(path, JSON.stringify({ ...manifest, version: '999.0.0' })) - expect(windowsPrebuildCacheIdentity({ ...options, repository }).key).toBe(before) - for (const field of Object.keys(manifest).filter((field) => field !== 'version')) { - writeFileSync(path, JSON.stringify({ ...manifest, [field]: 'changed' })) + const lockPath = join(repository, 'pnpm-lock.yaml') + const lock = readFileSync(lockPath, 'utf8') + for (const [from, to] of [ + [/(\n {2}node-pty@[^\n]+:\n {4}dependencies:\n {6}node-addon-api: )[^\n]+/, '$17.1.0'], + [/(\n {2}node-gyp@[^\n]+:\n {4}dependencies:\n)/, '$1 zwitch: 2.0.4\n'] + ]) { + expect(lock).toMatch(from) + writeFileSync(lockPath, lock.replace(from, to)) expect(windowsPrebuildCacheIdentity({ ...options, repository }).key).not.toBe(before) } }) it.each(cacheInputs)('invalidates a changed %s input', (file) => { - const repository = mkdtempSync(join(tmpdir(), 'orca-prebuild-key-')) - temporary.push(repository) - const source = resolve(import.meta.dirname, '../..') - for (const relative of cacheInputs) { - mkdirSync(dirname(join(repository, relative)), { recursive: true }) - cpSync(join(source, relative), join(repository, relative)) - } + const repository = repositoryCopy() const before = windowsPrebuildCacheIdentity({ ...options, repository }).key writeFileSync( join(repository, file), - file === 'package.json' - ? JSON.stringify({ - ...JSON.parse(readFileSync(join(repository, file), 'utf8')), - engines: { node: 'changed' } - }) - : `${readFileSync(join(repository, file), 'utf8')}\nchanged input` + `${readFileSync(join(repository, file), 'utf8')}\nchanged input` ) expect(windowsPrebuildCacheIdentity({ ...options, repository }).key).not.toBe(before) }) diff --git a/config/scripts/pr-code-change-scope.mjs b/config/scripts/pr-code-change-scope.mjs index 83b118e0af4..b90508659ef 100644 --- a/config/scripts/pr-code-change-scope.mjs +++ b/config/scripts/pr-code-change-scope.mjs @@ -328,15 +328,15 @@ const NATIVE_CACHE_FILES = new Set([ 'config/scripts/electron-platform-path.mjs', 'config/scripts/zip-extractor-command.mjs', 'src/shared/zip-extractor-command.ts', - 'config/scripts/shared-electron-dist-cache.mjs', - 'config/scripts/shared-tree-publish.mjs', 'config/scripts/space-sharing-copy.mjs', - 'config/scripts/native-build-entry.cjs', - 'config/scripts/native-build-entry-probe.cjs', - 'config/scripts/shared-native-build.mjs' + 'config/scripts/native-dependency-lock-digest.mjs', + 'config/scripts/orcad-prebuild-node-gyp.mjs', + 'config/scripts/msbuild-file-tracking.mjs' ]) const NATIVE_CACHE_PREFIXES = [ + 'config/scripts/native-build-entry', + 'config/scripts/shared-', 'config/patches/node-pty@', 'config/patches/@vscode__windows-process-tree', // The vendored addons' sources: build/ and bin/ beside them are ignored, so nothing else lands here. diff --git a/config/scripts/windows-rust-build-scope.mjs b/config/scripts/windows-rust-build-scope.mjs new file mode 100644 index 00000000000..8fbe8ca467a --- /dev/null +++ b/config/scripts/windows-rust-build-scope.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node +// Prints `toolchain=` and `app-version=` cache-key outputs for the Windows Rust executables. +// rustc goes in restore-key prefixes because the exe reuse fingerprint does not cover it. +import { spawnSync } from 'node:child_process' +import { readFileSync } from 'node:fs' +import { join, resolve } from 'node:path' + +function run(command, args) { + const result = spawnSync(command, args, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'] + }) + if (result.error || result.status !== 0) { + throw new Error( + `${command} ${args.join(' ')} failed: ${result.error?.message ?? result.status}` + ) + } + return result.stdout +} + +const root = resolve(import.meta.dirname, '../..') +if (spawnSync('cargo', ['--version'], { stdio: 'ignore' }).status !== 0) { + run('rustup', ['toolchain', 'install', 'stable', '--profile', 'minimal']) + run('rustup', ['default', 'stable']) +} +const rustc = /^rustc (\S+)/.exec(run('rustc', ['--version']))?.[1] +const { version } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) +if (!rustc || !version) { + throw new Error('Missing rustc or package.json version') +} +console.log(`toolchain=rustc${rustc}`) +console.log(`app-version=${version}`)