From ef242d5f17c57c1ce53f1b7f10a748b5ec750667 Mon Sep 17 00:00:00 2001 From: CSTRSK Date: Sat, 8 Aug 2026 21:39:44 +0000 Subject: [PATCH] fix: rollback plan shows new fleet IPs instead of old fleet In 'sh1pt scale rollout --rollback ', the rollback plan computed 'oldIps' by filtering fleet instances against target.newInstanceIds, so the displayed IPs were the NEW instances being torn down rather than the OLD fleet being restored. The actual rollback action correctly reactivates target.oldInstanceIds (blue-green), so the plan output contradicted what the command did. Extract rollbackPlanIps() which filters against oldInstanceIds and add regression tests covering publicIp, privateIp fallback and the missing-IP placeholder. --- packages/cli/src/commands/scale.test.ts | 46 +++++++++++++++++++++++++ packages/cli/src/commands/scale.ts | 11 ++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/scale.test.ts b/packages/cli/src/commands/scale.test.ts index e6a21cfe..ba208d44 100644 --- a/packages/cli/src/commands/scale.test.ts +++ b/packages/cli/src/commands/scale.test.ts @@ -16,6 +16,7 @@ import { parsePositiveNumber, parsePercentage, aggregateFleetCosts, + rollbackPlanIps, } from './scale.js'; // Helper to create a temp dir and override CREDS_FILE path @@ -375,3 +376,48 @@ describe('auto-scale rules', () => { expect(rules.cooldownSeconds).toBe(300); }); }); + +// --------------------------------------------------------------------------- +// rollbackPlanIps — rollback plan must list the OLD fleet, not the new one +// --------------------------------------------------------------------------- + +describe('rollbackPlanIps', () => { + const fleet: FleetState = { + instances: [ + { id: 'inst-0001', provider: 'aws', status: 'stopped', createdAt: '', hourlyRate: 0.1, publicIp: '10.0.0.1' }, + { id: 'inst-0002', provider: 'aws', status: 'stopped', createdAt: '', hourlyRate: 0.1, publicIp: '10.0.0.2' }, + { id: 'inst-0003', provider: 'aws', status: 'running', createdAt: '', hourlyRate: 0.1, publicIp: '10.1.0.1' }, + ], + }; + const rollout: RolloutRecord = { + id: 'roll-1', + version: 'v2', + strategy: 'blue-green', + status: 'completed', + startedAt: '2026-08-08T00:00:00.000Z', + newInstanceIds: ['inst-0003'], + oldInstanceIds: ['inst-0001', 'inst-0002'], + }; + + it('lists the old (pre-rollout) instances, not the new ones', () => { + const ips = rollbackPlanIps(fleet, rollout); + expect(ips).toEqual(['10.0.0.1', '10.0.0.2']); + expect(ips).not.toContain('10.1.0.1'); + }); + + it('falls back to privateIp when publicIp is missing', () => { + const fleetNoPub: FleetState = { + instances: [ + { id: 'inst-0001', provider: 'aws', status: 'stopped', createdAt: '', hourlyRate: 0.1, privateIp: '172.16.0.5' }, + ], + }; + expect(rollbackPlanIps(fleetNoPub, rollout)).toEqual(['172.16.0.5']); + }); + + it('returns placeholder for instances without any ip', () => { + const fleetNoIp: FleetState = { + instances: [{ id: 'inst-0001', provider: 'aws', status: 'stopped', createdAt: '', hourlyRate: 0.1 }], + }; + expect(rollbackPlanIps(fleetNoIp, rollout)).toEqual(['?.?.?.?']); + }); +}); diff --git a/packages/cli/src/commands/scale.ts b/packages/cli/src/commands/scale.ts index 556a000d..c57cc9f7 100644 --- a/packages/cli/src/commands/scale.ts +++ b/packages/cli/src/commands/scale.ts @@ -215,6 +215,13 @@ export function aggregateFleetCosts( return providerMap; } +/** Resolve the IPs of the old (pre-rollout) fleet for a rollback plan display. */ +export function rollbackPlanIps(fleet: FleetState, target: RolloutRecord): string[] { + return fleet.instances + .filter(i => target.oldInstanceIds.includes(i.id)) + .map(i => i.publicIp || i.privateIp || '?.?.?.?'); +} + export const scaleCmd = new Command('scale') .description('Provision + scale cloud infra. DNS round-robin, rollouts, rightsizing — all the capacity ops.') .option('--from ', 'existing live url, repo, or local path to probe + propose scaling for') @@ -728,9 +735,7 @@ scaleCmd process.exit(1); } const fleet = loadFleet(); - const oldIps = fleet.instances - .filter(i => target.newInstanceIds.includes(i.id)) - .map(i => i.publicIp || i.privateIp || '?.?.?.?'); + const oldIps = rollbackPlanIps(fleet, target); console.log(kleur.bold('\\nā® Rollback Plan')); console.log(kleur.dim('─'.repeat(56))); console.log(`${kleur.cyan('Rollout ID:'.padEnd(20))} ${target.id.slice(0, 8)} (v${target.version})`);