(rootDir: string, opts: ReleaseOptions)
| 545 | * when the detected state doesn't match expectations (used by split-job workflows). |
| 546 | */ |
| 547 | export async function ciReleaseCommand(rootDir: string, opts: ReleaseOptions): Promise<void> { |
| 548 | const config = await loadConfig(rootDir); |
| 549 | ensureGitIdentity(rootDir, config); |
| 550 | |
| 551 | // Snapshots are a one-shot transient release that can run from any branch (typically a |
| 552 | // feature PR), so they bypass the base/channel branch routing entirely. |
| 553 | if (opts.snapshot !== undefined) { |
| 554 | await ciSnapshotRelease(rootDir, opts); |
| 555 | return; |
| 556 | } |
| 557 | |
| 558 | // Channel branches get the channel flow; unknown branches are refused (when channels |
| 559 | // are configured) so a misconfigured workflow can't publish from a feature branch. |
| 560 | const releaseBranch = detectReleaseBranch(rootDir); |
| 561 | const channel = matchChannelByBranch(config, releaseBranch); |
| 562 | if (channel) { |
| 563 | await ciChannelRelease(rootDir, config, channel, opts); |
| 564 | return; |
| 565 | } |
| 566 | if (Object.keys(config.channels || {}).length > 0 && releaseBranch && releaseBranch !== config.baseBranch) { |
| 567 | throw new Error( |
| 568 | `"bumpy ci release" ran on branch "${releaseBranch}", which is neither the base branch ` + |
| 569 | `("${config.baseBranch}") nor a configured channel branch. Refusing to release — ` + |
| 570 | 'add the branch to "channels" in .bumpy/_config.json or fix the workflow trigger.', |
| 571 | ); |
| 572 | } |
| 573 | |
| 574 | const { packages } = await discoverWorkspace(rootDir, config); |
| 575 | const depGraph = new DependencyGraph(packages); |
| 576 | // Channel-dir bump files count as pending on the base branch — merging a channel |
| 577 | // into main brings its shipped files along, and the stable release consumes them |
| 578 | // (promotion). No special mode needed. |
| 579 | const { bumpFiles, errors: releaseParseErrors } = await readBumpFiles(rootDir, { channels: channelNames(config) }); |
| 580 | |
| 581 | if (releaseParseErrors.length > 0) { |
| 582 | for (const err of releaseParseErrors) { |
| 583 | log.error(err); |
| 584 | } |
| 585 | throw new Error('Bump file parse errors must be fixed before releasing.'); |
| 586 | } |
| 587 | |
| 588 | // Determine detected mode. "version-pr" = bump files exist with real releases. |
| 589 | // "publish" = no bump files, or only none-only files (version PR just merged). |
| 590 | const plan = bumpFiles.length > 0 ? assembleReleasePlan(bumpFiles, packages, depGraph, config) : null; |
| 591 | const detectedMode: 'version-pr' | 'publish' = plan && plan.releases.length > 0 ? 'version-pr' : 'publish'; |
| 592 | |
| 593 | if (opts.assertMode && opts.assertMode !== detectedMode) { |
| 594 | throw new Error( |
| 595 | `Expected mode "${opts.assertMode}" but detected "${detectedMode}". ` + |
| 596 | `Either remove --expect-mode, or gate this step on the output of "bumpy ci plan".`, |
| 597 | ); |
| 598 | } |
| 599 | |
| 600 | if (detectedMode === 'publish') { |
| 601 | // No bump files (or only none-only) — check for unpublished packages. |
| 602 | // Recover bump files deleted in the version commit so the formatter |
| 603 | // can generate proper GitHub release bodies. |
| 604 | const msg = |
no test coverage detected
searching dependent graphs…