diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c09d32d..c6cfad0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,13 @@ jobs: unit-test: uses: sxzz/workflows/.github/workflows/unit-test.yml@main with: + build: pnpm run ci:build + # The Build step above already produced a fresh dist/ for this exact + # checkout, so skip `test`'s own `build && vitest` — running plain + # vitest halves the number of full-monorepo `turbo run build` passes + # per job, which is where the flaky Windows native-toolchain crash + # (see scripts/ci-retry.ts) shows up. + test: pnpm exec vitest lint: pnpm run lint && pnpm run knip e2e: diff --git a/package.json b/package.json index 71514b55..5fdb4af9 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "bugs": "https://github.com/devframes/devframe/issues", "scripts": { "build": "turbo run build --concurrency=3", + "ci:build": "tsx scripts/ci-retry.ts pnpm run build", "watch": "pnpm -r run watch", "play": "tsx scripts/play.ts", "dev": "tsx scripts/play.ts", diff --git a/scripts/ci-retry.ts b/scripts/ci-retry.ts new file mode 100644 index 00000000..6edc094d --- /dev/null +++ b/scripts/ci-retry.ts @@ -0,0 +1,50 @@ +import { spawnSync } from 'node:child_process' +import process from 'node:process' + +/** + * Windows CI runners intermittently crash while spawning devframe's native + * build toolchain (rolldown, via tsdown/vite) with + * `STATUS_DLL_INIT_FAILED` (exit code -1073741502, surfaced by pnpm/turbo + * as 3221225794) under `turbo run build`'s concurrency — an environment + * fault unrelated to the code under test. `unit-test / test + * (windows-latest, *)` in the "CI" workflow has failed on this signature + * across many unrelated commits and packages. + * + * Retries the given command a bounded number of times instead of failing + * the run on this class of transient crash. A genuine compile error fails + * the same way on every attempt, so it still surfaces after the retry + * budget is spent. + */ + +const [, , ...commandParts] = process.argv +if (commandParts.length === 0) { + console.error('Usage: tsx scripts/ci-retry.ts ') + process.exit(1) +} + +const command = commandParts.join(' ') +const attempts = Number(process.env.CI_RETRY_ATTEMPTS ?? 3) +const delayMs = Number(process.env.CI_RETRY_DELAY_MS ?? 5000) + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +async function main(): Promise { + for (let attempt = 1; attempt <= attempts; attempt++) { + const result = spawnSync(command, { stdio: 'inherit', shell: true }) + if (result.status === 0) + return + + const code = result.status ?? result.signal ?? 'unknown' + const isLastAttempt = attempt === attempts + console.error(`\n[ci-retry] \`${command}\` failed (exit ${code}), attempt ${attempt}/${attempts}${isLastAttempt ? '' : ` — retrying in ${delayMs}ms`}\n`) + + if (isLastAttempt) + process.exit(typeof result.status === 'number' ? result.status : 1) + + await sleep(delayMs) + } +} + +main()