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"); });