From 7e54cc9d1933b0f80fa5d93aee9db6fcc15871da Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 14 Aug 2026 19:25:16 -0700 Subject: [PATCH] perf(ci): make push/PR preflight dependency-free and prune the store archive The manifest planner closure and the protocol coverage script import only node builtins and relative files (verified importing the full closure with an empty node_modules under native type stripping), so push/PR preflight drops the pnpm store restore and install (~30s off the barrier every lane waits behind). Manual dispatches keep the tsx path for frozen targets, and the coverage script inlines the record guard under the documented dependency-free exception. The store archive accretes every prior lockfile generation through prefix-key restores (measured 2.05 GiB, ~36s restore in every hosted job); the warmup writer now prunes to the current lockfile closure before saving. --- .github/actions/setup-node-env/action.yml | 13 +++++++++++++ .github/workflows/ci.yml | 22 ++++++++++++++++++++-- scripts/check-protocol-event-coverage.mts | 8 +++++++- test/scripts/ci-workflow-guards.test.ts | 5 ++++- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/.github/actions/setup-node-env/action.yml b/.github/actions/setup-node-env/action.yml index 7556e671fb5e..200978d0b465 100644 --- a/.github/actions/setup-node-env/action.yml +++ b/.github/actions/setup-node-env/action.yml @@ -425,6 +425,19 @@ runs: restore-keys: | ${{ github.repository }}-build-all-v1-${{ inputs.build-all-cache-scope }}-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}- + # Prune before saving: prefix-key restores accrete every prior lockfile + # generation into the archive (measured 2.05 GiB, ~36s restore per job). + # Pruning collapses it to the current lockfile's closure; a dropped entry + # costs one registry refetch in a later job at worst. + - name: Prune pnpm store before save + if: ${{ inputs.install-deps == 'true' && inputs.use-actions-cache == 'true' && (inputs.dependency-cache != 'true' || steps.dependency-cache.outputs.cache-hit != 'true') && inputs.save-actions-cache == 'true' && runner.os != 'Windows' && steps.setup-pnpm.outputs.store-cache-hit != 'true' }} + shell: bash + working-directory: ${{ steps.package-manager.outputs.project-dir }} + run: | + du -sh "${{ steps.setup-pnpm.outputs.store-path }}" || true + pnpm store prune + du -sh "${{ steps.setup-pnpm.outputs.store-path }}" || true + - name: Save pnpm store cache if: ${{ inputs.install-deps == 'true' && inputs.use-actions-cache == 'true' && (inputs.dependency-cache != 'true' || steps.dependency-cache.outputs.cache-hit != 'true') && inputs.save-actions-cache == 'true' && runner.os != 'Windows' && steps.setup-pnpm.outputs.store-cache-hit != 'true' }} uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e62f2f62b51a..1e87c5bc7bd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -418,12 +418,19 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} + # Push/PR preflight is dependency-free: the manifest planner closure and + # the protocol coverage script import only node builtins and relative + # files, and Node strips their types natively. Only manual dispatches + # (which may check out frozen targets with older planner syntax) still + # install and run through tsx. - name: Setup manifest pnpm + if: github.event_name == 'workflow_dispatch' uses: ./.github/actions/setup-pnpm-store-cache with: node-version: ${{ env.NODE_VERSION }} - name: Install manifest dependencies + if: github.event_name == 'workflow_dispatch' run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts - name: Build CI manifest @@ -455,7 +462,11 @@ jobs: OPENCLAW_CI_EVENT_NAME: ${{ github.event_name }} OPENCLAW_CI_RUNNER_BACKEND: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND }} run: | - node --import tsx --input-type=module <<'EOF' + manifest_node_args=() + if [ "${GITHUB_EVENT_NAME:-}" = "workflow_dispatch" ]; then + manifest_node_args+=(--import tsx) + fi + node "${manifest_node_args[@]}" --input-type=module <<'EOF' import { appendFileSync, existsSync, readFileSync } from "node:fs"; const eventName = process.env.OPENCLAW_CI_EVENT_NAME ?? ""; @@ -914,7 +925,14 @@ jobs: - name: Check mobile protocol event coverage if: steps.manifest.outputs.run_protocol_event_coverage == 'true' - run: node scripts/check-protocol-event-coverage.mjs + run: | + # Dispatches may target frozen checkouts whose script needs the tsx + # shim; push/PR checkouts run the dependency-free .mts natively. + if [ "${GITHUB_EVENT_NAME:-}" = "workflow_dispatch" ]; then + node scripts/check-protocol-event-coverage.mjs + else + node scripts/check-protocol-event-coverage.mts + fi # Publish one immutable semantic dependency archive before same-repo # Blacksmith jobs fan out. The GitHub backend uses the pnpm-store cache. diff --git a/scripts/check-protocol-event-coverage.mts b/scripts/check-protocol-event-coverage.mts index 89e3ccea408a..2a1d8dadc77f 100644 --- a/scripts/check-protocol-event-coverage.mts +++ b/scripts/check-protocol-event-coverage.mts @@ -19,7 +19,13 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { isRecord } from "@openclaw/normalization-core/record-coerce"; + +// Local record guard by design: preflight runs this script without installed +// dependencies (the dependency-free manifest contract), so the canonical +// @openclaw/normalization-core/record-coerce import cannot resolve here. +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} const GATEWAY_EVENTS_FILE = "src/gateway/server-methods-list.ts"; const GATEWAY_EVENT_CONSTANTS_FILE = "src/gateway/events.ts"; diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 1bd7885bbb76..30a512ad7836 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -5602,7 +5602,10 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" (step: WorkflowStep) => step.name === "Run check shard", ).run; - expect(coverageStep.run).toBe("node scripts/check-protocol-event-coverage.mjs"); + // Push/PR preflight is dependency-free and runs the .mts natively; + // dispatches (frozen targets) keep the tsx shim path. + expect(coverageStep.run).toContain("node scripts/check-protocol-event-coverage.mts"); + expect(coverageStep.run).toContain("node scripts/check-protocol-event-coverage.mjs"); expect(coverageStep.if).toBe("steps.manifest.outputs.run_protocol_event_coverage == 'true'"); expect(checkShardRun).not.toContain("check:protocol-coverage"); });