@formigio/fazemos-cli 0.10.54 → 0.10.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5640,6 +5640,94 @@ pipelines
|
|
|
5640
5640
|
process.exit(1);
|
|
5641
5641
|
}
|
|
5642
5642
|
});
|
|
5643
|
+
// ── OPERATOR-RECOVERY-BUNDLE — `pl reconcile <instanceId>` ──
|
|
5644
|
+
// Idempotent operator-safe healing command (spec §2).
|
|
5645
|
+
// Re-runs the DAG resolver, detects stranded steps (queued/in_progress with
|
|
5646
|
+
// no live execution), gates through canLaunch, re-launches eligible ones,
|
|
5647
|
+
// and returns a structured JSON report (launched, skipped, stalled).
|
|
5648
|
+
//
|
|
5649
|
+
// POST /api/pipeline-instances/:id/reconcile
|
|
5650
|
+
// Auth: owner or admin only
|
|
5651
|
+
//
|
|
5652
|
+
// Distinct from pl rematerialize (P-TOOL-1): rematerialize rebases onto the
|
|
5653
|
+
// TEMPLATE (schema/content-side); reconcile re-launches missing EXECUTIONS on
|
|
5654
|
+
// the instance as-is (runtime-side only). Different jobs kept separate.
|
|
5655
|
+
pipelines
|
|
5656
|
+
.command('reconcile')
|
|
5657
|
+
.description('Idempotent operator recovery: re-runs the DAG resolver, detects stranded steps ' +
|
|
5658
|
+
'(queued/in_progress with no live execution), and re-launches eligible ones. ' +
|
|
5659
|
+
'Safe to run multiple times — the second call fires nothing if the first succeeded. ' +
|
|
5660
|
+
'Owner/admin only. Distinct from rematerialize: this heals EXECUTIONS, not the template schema.')
|
|
5661
|
+
.argument('<instanceId>', 'Pipeline instance ID')
|
|
5662
|
+
.option('--json', 'Print the raw API response as JSON (machine-readable)')
|
|
5663
|
+
.action(async (instanceId, opts) => {
|
|
5664
|
+
try {
|
|
5665
|
+
const path = `/api/pipeline-instances/${instanceId}/reconcile`;
|
|
5666
|
+
const data = (await api('POST', path, {}));
|
|
5667
|
+
if (opts.json) {
|
|
5668
|
+
console.log(JSON.stringify(data, null, 2));
|
|
5669
|
+
return;
|
|
5670
|
+
}
|
|
5671
|
+
// Human-readable reconcile report
|
|
5672
|
+
const reconciledAt = data?.reconciled_at ? new Date(data.reconciled_at).toLocaleString() : 'unknown';
|
|
5673
|
+
console.log(chalk.bold(`Reconcile — instance ${instanceId}`));
|
|
5674
|
+
console.log(chalk.gray(` Reconciled at : ${reconciledAt}`));
|
|
5675
|
+
console.log(chalk.gray(` DAG resolver : ${data?.dag_resolver_ran ? 'ran' : 'skipped'}`));
|
|
5676
|
+
console.log(chalk.gray(` Steps verified: ${data?.steps_verified ?? 0}`));
|
|
5677
|
+
console.log();
|
|
5678
|
+
// Launched
|
|
5679
|
+
const launched = Array.isArray(data?.launched) ? data.launched : [];
|
|
5680
|
+
if (launched.length > 0) {
|
|
5681
|
+
console.log(chalk.green(` Launched (${launched.length}):`));
|
|
5682
|
+
for (const s of launched) {
|
|
5683
|
+
console.log(chalk.green(` ✓ ${s.step_name} (${s.step_id}) → execution ${s.execution_id}` +
|
|
5684
|
+
(s.assigned_to ? ` [agent: ${s.assigned_to}]` : '')));
|
|
5685
|
+
}
|
|
5686
|
+
}
|
|
5687
|
+
else {
|
|
5688
|
+
console.log(chalk.gray(' Launched: (none)'));
|
|
5689
|
+
}
|
|
5690
|
+
// Skipped (canLaunch=false due to pause/budget/fan-out)
|
|
5691
|
+
const skipped = Array.isArray(data?.skipped) ? data.skipped : [];
|
|
5692
|
+
if (skipped.length > 0) {
|
|
5693
|
+
console.log(chalk.yellow(`\n Skipped — canLaunch blocked (${skipped.length}):`));
|
|
5694
|
+
for (const s of skipped) {
|
|
5695
|
+
console.log(chalk.yellow(` ⚠ ${s.step_name} (${s.step_id}) [${s.reason}]: ${s.detail}`));
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
// Stalled (governance signal — queued, no execution, canLaunch=false for other reason)
|
|
5699
|
+
const stalled = Array.isArray(data?.stalled) ? data.stalled : [];
|
|
5700
|
+
if (stalled.length > 0) {
|
|
5701
|
+
console.log(chalk.red(`\n Stalled — governance signal (${stalled.length}):`));
|
|
5702
|
+
for (const s of stalled) {
|
|
5703
|
+
console.log(chalk.red(` ✗ ${s.step_name} (${s.step_id}) [status: ${s.status}]: ${s.detail}`));
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
console.log();
|
|
5707
|
+
if (launched.length === 0 && skipped.length === 0 && stalled.length === 0) {
|
|
5708
|
+
console.log(chalk.green('No stranded steps found — instance is healthy.'));
|
|
5709
|
+
}
|
|
5710
|
+
else {
|
|
5711
|
+
const summary = [];
|
|
5712
|
+
if (launched.length > 0)
|
|
5713
|
+
summary.push(chalk.green(`${launched.length} launched`));
|
|
5714
|
+
if (skipped.length > 0)
|
|
5715
|
+
summary.push(chalk.yellow(`${skipped.length} skipped`));
|
|
5716
|
+
if (stalled.length > 0)
|
|
5717
|
+
summary.push(chalk.red(`${stalled.length} stalled`));
|
|
5718
|
+
console.log(`Summary: ${summary.join(', ')}`);
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
catch (err) {
|
|
5722
|
+
// 403 owner/admin gate
|
|
5723
|
+
if (err instanceof ApiError && err.status === 403) {
|
|
5724
|
+
console.error(chalk.red(`Permission denied: ${err.message}`));
|
|
5725
|
+
process.exit(1);
|
|
5726
|
+
}
|
|
5727
|
+
console.error(chalk.red(err.message));
|
|
5728
|
+
process.exit(1);
|
|
5729
|
+
}
|
|
5730
|
+
});
|
|
5643
5731
|
pipelines
|
|
5644
5732
|
.command('set-params')
|
|
5645
5733
|
.description('Set instance parameters (atomic update)')
|