/** * Umbrella executor — runs a `PublishPlan` top to bottom. * * Mirrors the phase gating in plan.ts: if a per-phase items array is * empty, the corresponding execute is skipped. Workspace runs first * (its result feeds Phase 3's force-pin targets); the rest follow the * spec's Phase 2 → 3 → 4 ordering. After the workspace phase succeeds, * the build-bus fan-out emits webhooks to any registered subscribers * — best-effort, doesn't gate phases 2/3/4. */ import { eventsForPublished, fanOut, formatDeliveryResult, loadSubscribers, recordDeliveryOutcome, } from '../../../services/build-bus'; import { executeConsumerPins } from './consumer-pins'; import { executeGlobalUpdate } from './global-install'; import { currentGitHead } from './helpers'; import { executeModulePublish } from './module-registry'; import type { PublishPlan, PublishResult } from './types'; import { executeWorkspace } from './workspace'; export interface ExecutePlanInput { plan: PublishPlan; confirm: (question: string) => Promise; } export async function executePlan(input: ExecutePlanInput): Promise { const { plan, confirm } = input; // Build the workspace-versions map from the plan's items. The // planner already tightened versions for alpha mode; we just need // the lookup table for cross-package workspace:^ rewrites. const workspaceVersions = new Map(); for (const item of plan.workspace) { workspaceVersions.set(item.name, item.versionToPublish); } const result = await executeWorkspace({ items: plan.workspace, workspaceVersions, mode: plan.mode, confirm, }); // Build-bus fan-out — fires AFTER workspace publish succeeds, BEFORE // the optional follow-on phases. Best-effort: subscribers that fail // delivery don't block phases 2/3/4 from running. Operator sees the // outcome in the printed summary. if (result.published.length > 0) { await emitBuildBusEvents(result.published, plan); } if (plan.consumerPins.length > 0) { executeConsumerPins(plan.consumerPins); } if (plan.globalUpdate.length > 0) { executeGlobalUpdate(plan.globalUpdate); } if (plan.modulePublish.length > 0) { executeModulePublish(plan.modulePublish, { allowStale: plan.options.allowStale, skippedModules: plan.options.skippedModules, }); } return result; } /** * Best-effort build-bus fan-out. Loads the subscriber list, builds a * PublishEvent per just-published package, fires signed webhooks, * prints a delivery summary. Any failure (config parse, fan-out * error) is logged but doesn't crash the publish — subscribers are * an opt-in cross-machine relay, not a publish prerequisite. */ async function emitBuildBusEvents( published: PublishResult['published'], plan: PublishPlan, ): Promise { let subscribers: ReturnType; try { subscribers = loadSubscribers(); } catch (err) { console.warn( `\n⚠ build-bus: could not load subscriber config — ${err instanceof Error ? err.message : String(err)}`, ); console.warn(' (skipping fan-out; publish itself is unaffected)'); return; } if (subscribers.length === 0) return; // Mode determines the dist-tag carried on the event. Alpha + promote // both publish under specific tags (alpha → @alpha, promote → @latest // as a real release). Normal mode is @latest. const tag: 'latest' | 'alpha' = plan.mode.kind === 'alpha' ? 'alpha' : 'latest'; const gitHead = currentGitHead(); const events = eventsForPublished({ published, tag, gitHead, registry: 'npm', }); console.log('\n──────────────────────────────────────────────'); console.log(' Build-bus fan-out'); console.log('──────────────────────────────────────────────'); for (const event of events) { const results = await fanOut(event, subscribers); if (results.length === 0) { console.log( ` ${event.package.name}@${event.package.version}: no subscribers match (registry=${event.registry}, tag=${event.tag})`, ); continue; } console.log( ` ${event.package.name}@${event.package.version} → ${results.length} subscriber(s):`, ); for (const r of results) { console.log(` ${formatDeliveryResult(r)}`); // Persist outcome to the local bus so `celilo subscribers // status` can surface it later. Best-effort — never fails // the publish flow. recordDeliveryOutcome(event, r); } } }