diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index ac2e7f904e3..eb80d20af72 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -871,8 +871,17 @@ 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" + # Why: this install runs lifecycle scripts, so node-gyp rebuilds + # native/windows-registry and fetches that Node version's headers from + # nodejs.org. One `read ECONNRESET` there failed this blocking gate and the + # whole cut. Retry like the release build's install below. - name: Install dependencies - run: pnpm install --frozen-lockfile + uses: nick-fields/retry@v4 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: pnpm install --frozen-lockfile - name: Build Electron app for platform golden run: npx electron-vite build --mode e2e @@ -1088,8 +1097,14 @@ 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" + # Same node-gyp header fetch as the blocking golden gate above. - name: Install dependencies - run: pnpm install --frozen-lockfile + uses: nick-fields/retry@v4 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: pnpm install --frozen-lockfile - name: Build Electron app for terminal rendering evidence run: npx electron-vite build --mode e2e diff --git a/config/scripts/ci-dependency-download-cache.test.mjs b/config/scripts/ci-dependency-download-cache.test.mjs index d2111a230bb..0860b53ebca 100644 --- a/config/scripts/ci-dependency-download-cache.test.mjs +++ b/config/scripts/ci-dependency-download-cache.test.mjs @@ -32,25 +32,37 @@ describe('CI dependency download caches', () => { describe('release install targets', () => { const macCpuFlag = '--cpu=current,x64,arm64' // Both shapes: `run:` steps and steps wrapped in nick-fields/retry (`with.command`). + const installCommand = (step) => step.with?.command ?? step.run const installSteps = (name) => Object.values(workflow(name).jobs) .flatMap((job) => job.steps ?? []) - .map((step) => step.with?.command ?? step.run) - .filter((command) => typeof command === 'string' && command.includes('pnpm install ')) + .filter((step) => installCommand(step)?.includes('pnpm install ')) + const installCommands = (name) => installSteps(name).map(installCommand) it.each(['adhoc-mac-build', 'daily-mac-build', 'hourly-mac-build', 'release-mac-build'])( '%s installs both mac CPU variants for the x64+arm64 package config', (name) => { - const installs = installSteps(name) + const installs = installCommands(name) expect(installs.length).toBeGreaterThan(0) expect(installs.some((command) => command.includes(macCpuFlag))).toBe(true) } ) + // A transient `read ECONNRESET` fetching this Node version's headers for + // native/windows-registry's node-gyp rebuild failed a blocking golden gate and the cut. + it('retries every release-cut install so one transient download cannot fail a cut', () => { + const installs = installSteps('release-cut') + expect(installs.length).toBeGreaterThan(0) + for (const step of installs) { + expect(step.uses).toBe('nick-fields/retry@v4') + expect(step.with.max_attempts).toBeGreaterThan(1) + } + }) + it.each(['release-cut', 'dev-channel-win-build', 'windows-signing-rehearsal'])( '%s keeps installs scoped to the runner host', (name) => { - const installs = installSteps(name) + const installs = installCommands(name) expect(installs.length).toBeGreaterThan(0) for (const command of installs) { expect(command).not.toContain('--os=') diff --git a/src/main/skills/skill-upload-session-admission-regression.test.ts b/src/main/skills/skill-upload-session-admission-regression.test.ts index f34199b9aad..3d13a587889 100644 --- a/src/main/skills/skill-upload-session-admission-regression.test.ts +++ b/src/main/skills/skill-upload-session-admission-regression.test.ts @@ -16,7 +16,7 @@ const openGate = vi.hoisted(() => ({ })) // Models Windows delete-pending rmdir: the first removal wins and every later one gets EPERM. -const deletePendingGate = vi.hoisted(() => ({ removed: null as Set | null })) +const deletePendingGate = vi.hoisted((): { removed: Set | null } => ({ removed: null })) vi.mock('node:fs/promises', async (importOriginal) => { const actual = await importOriginal()