From 8d51b1907062a82aa4c4c560bd0368add38a3a77 Mon Sep 17 00:00:00 2001 From: Mao Haibo Date: Sun, 30 Aug 2026 20:42:02 +0800 Subject: [PATCH] fix(cli): assign unique rollout instance IDs --- packages/cli/src/commands/scale.test.ts | 42 ++++++++++++++++++++++++- packages/cli/src/commands/scale.ts | 2 +- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/scale.test.ts b/packages/cli/src/commands/scale.test.ts index 6a3065b8..bca070ce 100644 --- a/packages/cli/src/commands/scale.test.ts +++ b/packages/cli/src/commands/scale.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs'; +import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync, mkdirSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { @@ -324,6 +325,45 @@ describe('rollout strategies use getNextId correctly', () => { expect(newIds).toHaveLength(batchSize); expect(newIds[0]).toBe('inst-0007'); }); + + it('assigns unique IDs to every instance created by a blue-green rollout', () => { + const dir = makeTempDir(); + const stateDir = join(dir, '.sh1pt'); + mkdirSync(stateDir, { recursive: true }); + const statePath = join(stateDir, 'credentials.json'); + writeFileSync(statePath, JSON.stringify({ + apiKey: 'preserve-me', + instances: Array.from({ length: 3 }, (_, index) => ({ + id: `inst-${String(index + 1).padStart(4, '0')}`, + provider: 'aws', + status: 'running', + createdAt: '', + hourlyRate: 0.1, + })), + lastUpdated: '', + })); + + const scaleModule = new URL('./scale.ts', import.meta.url).href; + const runRollout = [ + `import { scaleCmd } from ${JSON.stringify(scaleModule)};`, + `await scaleCmd.parseAsync(['node', 'sh1pt', 'rollout', '--version', 'v2', '--strategy', 'blue-green']);`, + ].join('\n'); + const result = spawnSync( + process.execPath, + ['--import', 'tsx', '--input-type=module', '--eval', runRollout], + { + env: { ...process.env, HOME: dir, USERPROFILE: dir }, + encoding: 'utf8', + }, + ); + + expect(result.status, result.stderr).toBe(0); + const saved = JSON.parse(readFileSync(statePath, 'utf-8')); + expect(saved.apiKey).toBe('preserve-me'); + expect(saved.instances.slice(3).map((instance: FleetEntry) => instance.id)).toEqual([ + 'inst-0004', 'inst-0005', 'inst-0006', + ]); + }); }); // --------------------------------------------------------------------------- diff --git a/packages/cli/src/commands/scale.ts b/packages/cli/src/commands/scale.ts index 6ecf0c71..841f5015 100644 --- a/packages/cli/src/commands/scale.ts +++ b/packages/cli/src/commands/scale.ts @@ -820,7 +820,7 @@ scaleCmd const newInstances: FleetEntry[] = []; for (let i = 0; i < newInstanceCount; i++) { newInstances.push({ - id: getNextId(fleet.instances), + id: getNextId([...fleet.instances, ...newInstances]), provider: running.length > 0 ? running[0]!.provider : 'digitalocean', status: 'running', publicIp: `10.${base}.${1 + Math.floor(Math.random() * 254)}.${1 + Math.floor(Math.random() * 254)}`,