From 7f124672452503dcb490c53d436a2bb9394a9350 Mon Sep 17 00:00:00 2001 From: myelinated-wackerow <263208946+myelinated-wackerow@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:21:21 -0700 Subject: [PATCH] feat(intl-pipeline): partial-success runs ship; failures surface in PR Per-task failures no longer abort the whole run. The pool runs to completion; failures are captured with file context and rendered into the PR body as a "N task(s) failed" block with copy-pasteable rerun commands. Per-file manifests are still stamped only on success, so a rerun naturally retries only the failed combinations. Hard abort retained for the all-failed case (no empty PR). Co-Authored-By: Claude Opus 4.7 Co-Authored-By: wackerow <54227730+wackerow@users.noreply.github.com> --- .../lib/workflows/pr-creation.ts | 29 +++++++++- src/scripts/intl-pipeline/main.ts | 55 +++++++++++++++---- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/scripts/intl-pipeline/lib/workflows/pr-creation.ts b/src/scripts/intl-pipeline/lib/workflows/pr-creation.ts index 75643a96513..8a12b6a9129 100644 --- a/src/scripts/intl-pipeline/lib/workflows/pr-creation.ts +++ b/src/scripts/intl-pipeline/lib/workflows/pr-creation.ts @@ -43,6 +43,12 @@ function generateInitialPRBody(): string { ].join("\n") } +export interface RunFailure { + locale: string + file: string + message: string +} + /** * Generate a run summary to append to the PR body */ @@ -50,7 +56,8 @@ export function generateRunSummary( langCodes: string[], committedFiles: CommittedFile[], mode: string, - workflowRunUrl?: string + workflowRunUrl?: string, + failures: RunFailure[] = [] ): string { const now = new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC" @@ -71,6 +78,20 @@ export function generateRunSummary( parts.push(`- [View workflow run](${workflowRunUrl})`) } + if (failures.length > 0) { + parts.push("", `**${failures.length} task(s) failed:**`, "") + for (const f of failures) { + parts.push(`- \`${f.file}\` (${f.locale}): ${f.message}`) + } + parts.push("", "Rerun the failed combinations:", "", "```") + for (const f of failures) { + parts.push( + `gh workflow run "Intl Pipeline" -f target_path="${f.file}" -f target_languages="${f.locale}"` + ) + } + parts.push("```") + } + parts.push("") return parts.join("\n") } @@ -99,7 +120,8 @@ export async function createOrUpdateTranslationPR( branch: string, committedFiles: CommittedFile[], languagePairs: LanguagePair[], - mode: string + mode: string, + failures: RunFailure[] = [] ): Promise<{ number: number; html_url: string }> { logSection("Pull Request") @@ -109,7 +131,8 @@ export async function createOrUpdateTranslationPR( langCodes, committedFiles, mode, - workflowRunUrl + workflowRunUrl, + failures ) // Check for existing open PR diff --git a/src/scripts/intl-pipeline/main.ts b/src/scripts/intl-pipeline/main.ts index 6176aa8e0c2..23a1d58ae0a 100644 --- a/src/scripts/intl-pipeline/main.ts +++ b/src/scripts/intl-pipeline/main.ts @@ -706,6 +706,11 @@ async function main() { const committedFiles: Array<{ path: string; content: string }> = [] let hasCommits = false + // Per-task failures captured with file context. Populated by submitWithContext + // wrapper so we can report rich failure info in the PR body and surface + // copy-pasteable rerun commands. Pool's own error tracking still runs in + // parallel for the orchestration-level "did anything fail" check. + const failures: Array<{ locale: string; file: string; message: string }> = [] // Resolve target paths in five passes: // 1. Normalize (log-level: auto-prefix and strip accidental locale paths) @@ -783,6 +788,24 @@ async function main() { }, }) + // Wraps pool.submit to attach file context to any thrown error -- the pool + // only knows the locale; we want (locale, file, reason) for PR reporting. + const submitWithContext = ( + locale: string, + filePath: string, + fn: () => Promise + ) => { + pool.submit(locale, async () => { + try { + return await fn() + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + failures.push({ locale, file: filePath, message }) + throw err + } + }) + } + // Submit all file x language tasks to the pool for (const file of englishFiles) { for (const locale of targetLanguages) { @@ -815,7 +838,7 @@ async function main() { continue } - pool.submit(locale, () => + submitWithContext(locale, file.path, () => runFullTranslation( file, locale, @@ -839,7 +862,7 @@ async function main() { if (config.stampOnly) { log(`[${locale}] ${file.path}: stamp only`) - pool.submit(locale, async () => { + submitWithContext(locale, file.path, async () => { const sourceManifest = file.type === "markdown" ? buildMarkdownManifest(file.content, file.path, baseBranchSha) @@ -854,7 +877,7 @@ async function main() { continue } - pool.submit(locale, () => + submitWithContext(locale, file.path, () => runIncremental( file, locale, @@ -872,15 +895,24 @@ async function main() { // Wait for all tasks to complete await pool.drain() - // Check for task failures - if (pool.hasErrors()) { - const errors = pool.getErrors() - console.error(`[pipeline] ${errors.length} task(s) failed:`) - for (const { language, error } of errors) { - console.error(` [${language}] ${error.message}`) + // Log task failures but don't abort -- partial successes ship. Failures are + // recorded with file context (via submitWithContext) and surfaced in the PR + // body with rerun commands. Per-file manifests are only stamped on success, + // so a rerun of just the failed combinations naturally retries them without + // touching the work that landed this run. + if (failures.length > 0) { + log(`${failures.length} task(s) failed (continuing with successes):`, "warn") + for (const f of failures) { + log(` [${f.locale}] ${f.file}: ${f.message}`, "warn") } + } + + // Hard abort only if literally nothing succeeded -- a fully-failed run + // shouldn't produce an empty PR. (committedFiles excludes manifest-only stamp + // commits, so we also check hasCommits for the stamp-only path.) + if (failures.length > 0 && committedFiles.length === 0 && !hasCommits) { throw new Error( - `Pipeline aborted: ${errors.length} translation task(s) failed. Temp branch ${tempBranch} preserved with partial progress.` + `Pipeline aborted: all ${failures.length} translation task(s) failed. Temp branch ${tempBranch} preserved.` ) } @@ -942,7 +974,8 @@ async function main() { targetBranch, committedFiles, languagePairs, - config.mode + config.mode, + failures ) } catch (error) { console.warn(