Ver código fonte

ci: collapse cold setup waves

Tianyi Cui 2 meses atrás
pai
commit
0f57eafc36
3 arquivos alterados com 71 adições e 51 exclusões
  1. 45 48
      .github/workflows/ci.yml
  2. 1 0
      package.json
  3. 25 3
      scripts/run-gates.ts

+ 45 - 48
.github/workflows/ci.yml

@@ -319,7 +319,7 @@ jobs:
         run: ${{ matrix.command }}
 
   # One large runner pays hosted setup once, then the repository scheduler
-  # overlaps the complete unsharded primary gate inventory inside that box.
+  # overlaps the unsharded primary inventory except the production site build.
   node-24:
     if: github.event_name != 'workflow_dispatch' || inputs.suite == 'optimized-larger-runners'
     runs-on: dsh-ubuntu-24-04-96core
@@ -383,6 +383,38 @@ jobs:
       - name: Run complete unsharded primary Node CI concurrently
         run: pnpm run check:ci:large-runner
 
+  # Keep only the longest independent Linux gate on a second coarse-grained
+  # runner so cold install variance does not push the primary box over a minute.
+  node-24-site:
+    if: github.event_name != 'workflow_dispatch' || inputs.suite == 'optimized-larger-runners'
+    runs-on: dsh-ubuntu-24-04-16core
+    name: node 24 / production site
+    steps:
+      - uses: actions/checkout@v6
+
+      - uses: actions/setup-node@v6
+        with:
+          node-version: ${{ env.PRIMARY_NODE_VERSION }}
+
+      - name: Enable corepack and resolve pnpm store path
+        id: pnpm-store
+        run: |
+          corepack enable
+          echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
+
+      - uses: actions/cache@v4
+        with:
+          path: ${{ steps.pnpm-store.outputs.path }}
+          key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
+          restore-keys: |
+            ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
+
+      - name: Install (immutable)
+        run: pnpm install --frozen-lockfile
+
+      - name: Build documentation site (production SPA)
+        run: pnpm run docs:build
+
   node-compat:
     if: github.event_name != 'workflow_dispatch' || inputs.suite == 'optimized-larger-runners'
     runs-on: ubuntu-latest
@@ -583,49 +615,13 @@ jobs:
         shell: pwsh
         run: ${{ matrix.command }}
 
-  # The two blocking Windows build surfaces share one setup and run concurrently.
-  windows-build:
+  # One Windows box shares setup across the required build/site checks and the
+  # complete observational portability inventory. run-gates reports failures
+  # from observational gates without allowing them to fail the required job.
+  windows:
     if: github.event_name != 'workflow_dispatch' || inputs.suite == 'optimized-larger-runners'
-    runs-on: dsh-windows-2025-16core
-    name: windows / blocking builds
-    steps:
-      - uses: actions/checkout@v6
-
-      - uses: actions/setup-node@v6
-        with:
-          node-version: ${{ env.PRIMARY_NODE_VERSION }}
-
-      - name: Enable corepack and resolve pnpm store path
-        id: pnpm-store
-        shell: pwsh
-        run: |
-          corepack enable
-          "path=$(pnpm store path --silent)" >> $env:GITHUB_OUTPUT
-
-      - uses: actions/cache@v4
-        with:
-          path: ${{ steps.pnpm-store.outputs.path }}
-          key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
-          restore-keys: |
-            ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
-
-      - name: Install (immutable)
-        shell: pwsh
-        run: pnpm install --frozen-lockfile
-
-      - name: Run blocking Windows builds concurrently
-        shell: pwsh
-        env:
-          DSH_GATE_CONCURRENCY: '2'
-        run: pnpm run check:ci:windows-blocking
-
-  # Wider Windows portability gates remain observational, but one larger box
-  # now runs their complete unsharded inventory with in-runner concurrency.
-  windows-gates:
-    if: github.event_name != 'workflow_dispatch' || inputs.suite == 'optimized-larger-runners'
-    continue-on-error: true
     runs-on: dsh-windows-2025-32core
-    name: windows node 24 / observational
+    name: windows node 24 / complete
     env:
       # Keep ESLint itself single-threaded: 16 ESLint workers took 174 seconds on
       # this image. The outer scheduler still overlaps lint with the other gates.
@@ -670,9 +666,9 @@ jobs:
         shell: pwsh
         run: pnpm install --frozen-lockfile
 
-      - name: Run observational Windows gates concurrently
+      - name: Run blocking and observational Windows gates concurrently
         shell: pwsh
-        run: pnpm run check:ci:windows-observational
+        run: pnpm run check:ci:windows-complete
 
   # Manual reference runs deliberately avoid the optimized jobs above.
   # Each host executes the complete, unsharded primary Node aggregate with one
@@ -1011,7 +1007,7 @@ jobs:
           DSH_GATE_CONCURRENCY: ${{ matrix.workers }}
           DSH_PUBLINT_CONCURRENCY: ${{ matrix.workers }}
           DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ matrix.workers }}
-        run: pnpm run check:ci:large-runner
+        run: pnpm run check:ci
 
       - name: Run blocking Windows builds concurrently
         if: matrix.platform == 'windows'
@@ -1023,8 +1019,9 @@ jobs:
   # Single stable required check for branch protection: require "all checks
   # passed" instead of enumerating matrix legs whose names change as lanes and
   # node versions evolve. Every blocking job in THIS workflow must be listed in
-  # `needs`; explicitly observational jobs such as windows-gates stay out
-  # (`needs` cannot reach across workflow files; e2e.yml stays its own check).
+  # `needs`; observational Windows gates share the required Windows job but are
+  # marked non-blocking inside run-gates. (`needs` cannot reach across workflow
+  # files; e2e.yml stays its own check.)
   # `if: always()` is load-bearing: without it a failed dependency
   # would SKIP this job, and GitHub counts a skipped required check as passing
   # — so this job always runs and fails on any non-success result, including
@@ -1032,7 +1029,7 @@ jobs:
   all-checks-passed:
     name: all checks passed
     runs-on: ubuntu-latest
-    needs: [node-24, node-compat, python-sdk, windows-build]
+    needs: [node-24, node-24-site, node-compat, python-sdk, windows]
     if: always() && (github.event_name != 'workflow_dispatch' || inputs.suite == 'optimized-larger-runners')
     steps:
       - name: Fail if any needed job did not succeed

+ 1 - 0
package.json

@@ -33,6 +33,7 @@
     "check:ci:snapshot": "tsx scripts/run-gates.ts ci-snapshot",
     "check:ci:artifacts": "tsx scripts/run-gates.ts ci-artifacts",
     "check:ci:windows-blocking": "tsx scripts/run-gates.ts ci-windows-blocking",
+    "check:ci:windows-complete": "tsx scripts/run-gates.ts ci-windows-complete",
     "check:ci:windows-observational": "tsx scripts/run-gates.ts ci-windows-observational",
     "check:node-compat": "tsx scripts/run-gates.ts node-compat",
     "check:pre-push": "tsx scripts/run-gates.ts pre-push",

+ 25 - 3
scripts/run-gates.ts

@@ -22,6 +22,7 @@ type Mode =
   | 'ci-snapshot'
   | 'ci-artifacts'
   | 'ci-windows-blocking'
+  | 'ci-windows-complete'
   | 'ci-windows-observational'
   | 'node-compat'
   | 'pre-push'
@@ -38,6 +39,7 @@ interface Gate {
   env?: Record<string, string | undefined>
   input?: string
   verify?: (result: GateResult) => Promise<void>
+  allowFailure?: boolean
 }
 
 interface GateResult {
@@ -83,7 +85,9 @@ console.log(`run-gates: ${mode} running ${gates.length} gate(s) with ${maxConcur
 const results = await runGates(gates, maxConcurrency)
 printSummary(results, performance.now() - startedAt)
 
-if (results.some(result => result.status === 'failed' || result.status === 'skipped')) process.exit(1)
+if (results.some(result => result.gate.allowFailure !== true && (result.status === 'failed' || result.status === 'skipped'))) {
+  process.exit(1)
+}
 
 function parseMode(raw: string | undefined): Mode {
   switch (raw) {
@@ -95,6 +99,7 @@ function parseMode(raw: string | undefined): Mode {
     case 'ci-snapshot':
     case 'ci-artifacts':
     case 'ci-windows-blocking':
+    case 'ci-windows-complete':
     case 'ci-windows-observational':
     case 'node-compat':
     case 'pre-push':
@@ -102,7 +107,7 @@ function parseMode(raw: string | undefined): Mode {
       return raw
     default:
       throw new Error(
-        `run-gates: expected mode ci-primary | ci-primary-large-runner | ci-static | ci-lint | ci-coverage | ci-snapshot | ci-artifacts | ci-windows-blocking | ci-windows-observational | node-compat | pre-push | doc-sync, got ${JSON.stringify(raw)}.`,
+        `run-gates: expected mode ci-primary | ci-primary-large-runner | ci-static | ci-lint | ci-coverage | ci-snapshot | ci-artifacts | ci-windows-blocking | ci-windows-complete | ci-windows-observational | node-compat | pre-push | doc-sync, got ${JSON.stringify(raw)}.`,
       )
   }
 }
@@ -189,6 +194,8 @@ function gatesForMode(selected: Mode): Gate[] {
       return ciArtifactGates()
     case 'ci-windows-blocking':
       return ciWindowsBlockingGates()
+    case 'ci-windows-complete':
+      return ciWindowsCompleteGates()
     case 'ci-windows-observational':
       return ciWindowsObservationalGates()
     case 'node-compat':
@@ -254,6 +261,7 @@ function ciPrimaryLargeRunnerGates(): Gate[] {
   // Typecheck does not consume build output, so a large runner can start both
   // together while snapshot and artifact consumers still wait for the build.
   return ciPrimaryGates()
+    .filter(gate => gate.id !== 'docs-site-build')
     .map((gate) => {
       if (gate.id !== 'build') return gate
       const eagerBuild = { ...gate }
@@ -306,6 +314,19 @@ function ciWindowsBlockingGates(): Gate[] {
   ]
 }
 
+function ciWindowsCompleteGates(): Gate[] {
+  const observational = ciWindowsObservationalGates()
+    // The required production site replaces the observational MPA build; both
+    // VitePress modes write the same output directory and cannot overlap.
+    .filter(gate => gate.id !== 'build' && gate.id !== 'docs-site-build')
+    .map(gate => ({ ...gate, allowFailure: true }))
+  return [
+    pnpmScript('build', 'build'),
+    pnpmScript('windows-site', 'docs:build', { label: 'production site' }),
+    ...observational,
+  ]
+}
+
 function ciWindowsObservationalGates(): Gate[] {
   return [
     ...ciStaticGates(),
@@ -638,7 +659,8 @@ function printSummary(results: GateResult[], durationMs: number): void {
   for (const result of unsuccessful) {
     const duration = (result.durationMs / 1000).toFixed(2)
     const reason = result.error ?? (result.exitCode === null ? 'no exit code' : `exit ${result.exitCode}`)
-    console.error(`  - ${result.status.toUpperCase()} ${result.gate.label} (${duration}s, ${reason})`)
+    const disposition = result.gate.allowFailure === true ? 'NON-BLOCKING ' : ''
+    console.error(`  - ${disposition}${result.status.toUpperCase()} ${result.gate.label} (${duration}s, ${reason})`)
     console.error(`    ${result.gate.displayCommand}`)
   }
 }