Files
next.js/scripts/pull-turbo-cache.js
T
JJ KasperandGitHub 1bf6c2f4b1 Fix flaky turbo cache pull with retries and graceful fallback (#89575)
- Add retry logic (3 attempts with 5s delay) for transient network
failures
- Don't fail the job when cache restoration fails - let workflow build
from source
- Add error handling for dry run failures and JSON parsing

This attempts to address transient failures seen here
https://github.com/vercel/next.js/actions/runs/21735816168/job/62700523424?pr=89574
2026-02-06 15:06:40 +00:00

127 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
// @ts-check
const { spawn } = require('child_process')
const MAX_ATTEMPTS = 3
const RETRY_DELAY_MS = 5000
/**
* @param {number} ms
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* @param {string} command
* @param {{ stdio?: 'pipe' | 'inherit', captureOutput?: boolean }} options
* @returns {Promise<{ code: number | null, signal: string | null, output: string }>}
*/
function runCommand(
command,
{ stdio = 'inherit', captureOutput = false } = {}
) {
return new Promise((resolve) => {
let output = ''
const child = spawn('/bin/bash', ['-c', command], {
stdio: captureOutput ? 'pipe' : stdio,
})
if (captureOutput) {
child.stdout?.on('data', (data) => {
process.stdout.write(data)
output += data.toString()
})
child.stderr?.on('data', (data) => {
process.stderr.write(data)
})
}
child.on('exit', (code, signal) => {
resolve({ code, signal, output })
})
})
}
/**
* @param {string} command
* @param {number} maxAttempts
* @returns {Promise<boolean>}
*/
async function runWithRetry(command, maxAttempts) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
console.log(`Attempt ${attempt}/${maxAttempts}...`)
const { code, signal } = await runCommand(command)
if (!code && !signal) {
return true // success
}
console.warn(
`Attempt ${attempt} failed (exit code ${code}, signal ${signal})`
)
if (attempt < maxAttempts) {
console.log(`Retrying in ${RETRY_DELAY_MS / 1000}s...`)
await sleep(RETRY_DELAY_MS)
}
}
return false // all attempts failed
}
;(async function () {
const target = process.argv[process.argv.length - 1]
const turboCommand = `pnpm dlx turbo@${process.env.TURBO_VERSION || 'latest'}`
// First, do a dry run to check cache status
const { code, signal, output } = await runCommand(
`${turboCommand} run cache-build-native --dry=json -- ${target}`,
{ captureOutput: true }
)
if (code || signal) {
console.warn(
`Dry run failed (exit code ${code}, signal ${signal}). Continuing without cache.`
)
return
}
let turboData
try {
turboData = JSON.parse(output)
} catch (e) {
console.warn(`Failed to parse turbo output: ${e.message}`)
return
}
const task = turboData.tasks.find((t) => t.command !== '<NONEXISTENT>')
if (!task) {
console.warn(`Failed to find related turbo task`, output)
return
}
// Pull cache if it was available
if (task.cache.local || task.cache.remote) {
console.log('Cache Status', task.taskId, task.hash, task.cache)
const success = await runWithRetry(
`${turboCommand} run cache-build-native -- ${target}`,
MAX_ATTEMPTS
)
if (!success) {
// Don't fail the job - the workflow will check if build exists
// and build from source if needed
console.warn(
`Cache restoration failed after ${MAX_ATTEMPTS} attempts. ` +
`Build will proceed from source.`
)
}
} else {
console.warn(`No turbo cache was available, continuing...`)
console.warn(task)
}
})()