-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(ci): make release workflow retry-safe and resilient #5810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+234
−31
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| /** | ||
| * Resilient per-package publish script. | ||
| * | ||
| * Unlike `pnpm -r publish`, this script: | ||
| * - Skips packages that are already published on npm (safe for retries) | ||
| * - Publishes each package individually so one failure doesn't block others | ||
| * - Retries failed packages once | ||
| * - Exits 0 only when all packages are published successfully | ||
| * | ||
| * Usage: | ||
| * node scripts/publish.js --tag=latest [--provenance] [--dry-run] | ||
| */ | ||
|
|
||
| import { execFileSync } from 'node:child_process'; | ||
| import path from 'node:path'; | ||
|
|
||
| import { getPublishablePackages } from './utils.js'; | ||
|
|
||
| const args = process.argv.slice(2); | ||
| const isDryRun = args.includes('--dry-run'); | ||
| const useProvenance = args.includes('--provenance'); | ||
|
|
||
| let npmTag = 'latest'; | ||
| const tagArg = args.find(arg => arg.startsWith('--tag=')); | ||
| if (tagArg) { | ||
| npmTag = tagArg.split('=')[1]; | ||
| } | ||
|
|
||
| const baseDir = path.join(import.meta.dirname, '..'); | ||
| const packages = getPublishablePackages(baseDir); | ||
|
|
||
| console.log(`📦 Publishing ${packages.length} packages (tag: ${npmTag}${isDryRun ? ', dry-run' : ''}${useProvenance ? ', provenance' : ''})`); | ||
|
|
||
| /** | ||
| * Check if a specific version of a package is already published on npm. | ||
| */ | ||
| function isPublished(name, version) { | ||
| try { | ||
| const result = execFileSync('npm', ['view', `${name}@${version}`, 'version'], { | ||
| encoding: 'utf8', | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| timeout: 15000, | ||
| }).trim(); | ||
| return result === version; | ||
| } catch { | ||
| // Could be 404 (not published) or network error. | ||
| // Either way, we should attempt to publish. | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Publish a single package using pnpm --filter (preserves workspace context | ||
| * so that workspace: protocol references are properly resolved). | ||
| */ | ||
| function publishOne(pkg) { | ||
| const publishArgs = [ | ||
| '--filter', pkg.name, | ||
| 'publish', '--no-git-checks', '--access', 'public', '--tag', npmTag, | ||
| ]; | ||
| if (useProvenance) publishArgs.push('--provenance'); | ||
| if (isDryRun) publishArgs.push('--dry-run'); | ||
|
|
||
| execFileSync('pnpm', publishArgs, { | ||
| cwd: baseDir, | ||
| stdio: 'inherit', | ||
| env: { ...process.env, NPM_CONFIG_LOGLEVEL: 'verbose' }, | ||
| timeout: 120000, | ||
| }); | ||
| } | ||
|
|
||
| const published = []; | ||
| const skipped = []; | ||
| const toRetry = []; | ||
|
|
||
| for (const pkg of packages) { | ||
| const label = `${pkg.name}@${pkg.version}`; | ||
|
|
||
| // Skip packages already on npm (safe for retries) | ||
| if (!isDryRun && isPublished(pkg.name, pkg.version)) { | ||
| console.log(` ⏭️ ${label} already published`); | ||
| skipped.push(label); | ||
| continue; | ||
| } | ||
|
|
||
| try { | ||
| publishOne(pkg); | ||
| console.log(` ✅ ${label}`); | ||
| published.push(label); | ||
| } catch { | ||
| // Double-check: the publish might have actually succeeded | ||
| // (e.g. npm returned non-zero but the package landed) | ||
| if (!isDryRun && isPublished(pkg.name, pkg.version)) { | ||
| console.log(` ⏭️ ${label} already published (confirmed after error)`); | ||
| skipped.push(label); | ||
| } else { | ||
| console.error(` ❌ ${label} failed, will retry`); | ||
| toRetry.push(pkg); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Retry failed packages once | ||
| const finalFailed = []; | ||
| if (toRetry.length > 0 && !isDryRun) { | ||
| console.log(`\n🔄 Retrying ${toRetry.length} failed package(s)...`); | ||
|
|
||
| for (const pkg of toRetry) { | ||
| const label = `${pkg.name}@${pkg.version}`; | ||
|
|
||
| if (isPublished(pkg.name, pkg.version)) { | ||
| console.log(` ⏭️ ${label} now published`); | ||
| skipped.push(label); | ||
| continue; | ||
| } | ||
|
|
||
| try { | ||
| publishOne(pkg); | ||
| console.log(` ✅ ${label} (retry)`); | ||
| published.push(label); | ||
| } catch { | ||
| if (isPublished(pkg.name, pkg.version)) { | ||
| console.log(` ⏭️ ${label} now published (confirmed after retry error)`); | ||
| skipped.push(label); | ||
| } else { | ||
| console.error(` ❌ ${label} retry failed`); | ||
| finalFailed.push(label); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Summary | ||
| console.log('\n📊 Publish Summary:'); | ||
| console.log(` Published: ${published.length}`); | ||
| console.log(` Skipped: ${skipped.length}`); | ||
| console.log(` Failed: ${finalFailed.length}`); | ||
|
|
||
| if (finalFailed.length > 0) { | ||
| console.error('\n❌ Failed packages:'); | ||
| for (const label of finalFailed) { | ||
| console.error(` - ${label}`); | ||
| } | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log('\n✅ All packages published successfully!'); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dry-run failures are currently swallowed and reported as success.
Failed dry-run publishes are only queued in
toRetry, but retry is skipped in dry-run mode, sofinalFailedstays empty and the script exits 0 with a success message.💡 Suggested fix
Also applies to: 141-149
🤖 Prompt for AI Agents