@payfit/iac 1.0.3 → 1.0.4-ephemeral-iac-migration-docs.1

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.
Files changed (24) hide show
  1. package/dist/executors/stack-migration/README.md +71 -115
  2. package/dist/executors/stack-migration/lib/migration-tool.d.ts +6 -10
  3. package/dist/executors/stack-migration/lib/migration-tool.js +220 -218
  4. package/dist/executors/stack-migration/lib/migration-tool.js.map +1 -1
  5. package/dist/executors/stack-migration/lib/templates/index.d.ts +1 -0
  6. package/dist/executors/stack-migration/lib/templates/index.js +1 -0
  7. package/dist/executors/stack-migration/lib/templates/index.js.map +1 -1
  8. package/dist/executors/stack-migration/lib/templates/mermaid-diagram.d.ts +13 -0
  9. package/dist/executors/stack-migration/lib/templates/mermaid-diagram.js +46 -0
  10. package/dist/executors/stack-migration/lib/templates/mermaid-diagram.js.map +1 -0
  11. package/dist/executors/stack-migration/lib/templates/message-templates.d.ts +15 -20
  12. package/dist/executors/stack-migration/lib/templates/message-templates.js +34 -93
  13. package/dist/executors/stack-migration/lib/templates/message-templates.js.map +1 -1
  14. package/dist/executors/stack-migration/lib/templates/pr-templates.d.ts +16 -9
  15. package/dist/executors/stack-migration/lib/templates/pr-templates.js +77 -121
  16. package/dist/executors/stack-migration/lib/templates/pr-templates.js.map +1 -1
  17. package/dist/executors/stack-migration/lib/templates/procedure-template.d.ts +1 -0
  18. package/dist/executors/stack-migration/lib/templates/procedure-template.js +37 -118
  19. package/dist/executors/stack-migration/lib/templates/procedure-template.js.map +1 -1
  20. package/dist/executors/stack-migration/schema.d.ts +2 -3
  21. package/dist/executors/stack-migration/schema.json +2 -3
  22. package/dist/executors/stack-migration/stack-migration.js +3 -6
  23. package/dist/executors/stack-migration/stack-migration.js.map +1 -1
  24. package/package.json +1 -1
@@ -9,9 +9,8 @@ const spacelift_client_1 = require("./spacelift-client");
9
9
  const templates_1 = require("./templates");
10
10
  class MigrationTool extends migration_helper_1.MigrationHelper {
11
11
  async setupRepositories(options) {
12
- this.logger.step(1, 9, 'Repository Setup and Validation');
13
- // Check spacectl CLI installation first
14
- this.logger.info('\nChecking spacectl CLI installation...');
12
+ this.logger.step(1, 9, 'Repository Setup');
13
+ this.logger.info('Checking spacectl CLI...');
15
14
  await this.ensureSpacectlInstalled();
16
15
  if (this.config.resuming &&
17
16
  this.config.serviceRepository.name &&
@@ -34,7 +33,7 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
34
33
  .map(g => g.trim())
35
34
  .filter(g => g.length > 0);
36
35
  }
37
- this.logger.info('\nCloning required repositories...');
36
+ this.logger.info('Cloning repositories...');
38
37
  if (!this.files.exists(this.paths.tmpDir)) {
39
38
  this.files.mkdir(this.paths.tmpDir, { recursive: true });
40
39
  }
@@ -42,24 +41,30 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
42
41
  const repoPath = this.paths.clonedRepo(key);
43
42
  // Check if already cloned
44
43
  if (this.files.exists(repoPath)) {
45
- this.logger.info(`Repository ${key} already cloned, pulling latest...`);
46
- if (!this.dryRun) {
44
+ if (this.dryRun) {
45
+ this.logger.info(`[DRY RUN] Would pull ${key}`);
46
+ }
47
+ else {
48
+ this.logger.info(`${key} already cloned, pulling latest`);
47
49
  await this.git.pull(repoPath);
48
50
  }
49
51
  }
50
52
  else {
51
- await this.git.clone(url, repoPath, this.paths.tmpDir);
52
- this.logger.success(`Cloned ${key}`);
53
+ if (this.dryRun) {
54
+ this.logger.info(`[DRY RUN] Would clone ${key}`);
55
+ }
56
+ else {
57
+ await this.git.clone(url, repoPath, this.paths.tmpDir);
58
+ this.logger.success(`Cloned ${key}`);
59
+ }
53
60
  }
54
61
  this.config.repositories[key] =
55
62
  repoPath;
56
63
  }
57
- // Check spacectl authentication
58
- this.logger.info('\nChecking spacectl authentication...');
64
+ this.logger.info('Checking spacectl authentication...');
59
65
  await this.ensureSpacectlAuthentication();
60
- this.logger.success('All repositories cloned and ready');
61
- // Update .gitignore to exclude migration temporary files
62
- this.logger.info('\nUpdating .gitignore...');
66
+ this.logger.success('Repositories ready');
67
+ this.logger.info('Updating .gitignore...');
63
68
  const gitignorePath = this.paths.serviceRepoGitignore();
64
69
  let gitignoreContent = '';
65
70
  if (this.files.exists(gitignorePath)) {
@@ -69,18 +74,13 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
69
74
  if (!gitignoreContent.includes('.migration-tmp/')) {
70
75
  gitignoreContent += '\n' + constants_1.GITIGNORE_ENTRIES;
71
76
  this.files.writeFile(gitignorePath, gitignoreContent);
72
- if (!this.dryRun) {
73
- this.logger.success('Updated .gitignore with migration entries');
74
- }
75
- }
76
- else {
77
- this.logger.info('.gitignore already contains migration entries');
78
77
  }
79
78
  // Save initial state (only set timestamp if not resuming)
80
79
  if (!this.config.migrationStartedAt) {
81
80
  this.config.migrationStartedAt = new Date().toISOString();
82
81
  }
83
82
  this.saveMigrationState();
83
+ this.logger.success('Repository setup complete');
84
84
  }
85
85
  /**
86
86
  * Step 2: Load Configuration
@@ -99,21 +99,21 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
99
99
  if (!this.files.exists(iacConfigPath)) {
100
100
  throw new Error(`Stack configuration not found at: ${iacConfigPath}`);
101
101
  }
102
- this.logger.info(`Reading stack configuration from iac-deploy...`);
102
+ this.logger.info('Reading stack configuration...');
103
103
  const iacConfig = this.files.readYaml(iacConfigPath);
104
104
  // Extract stacks
105
105
  if (iacConfig.stacks && Array.isArray(iacConfig.stacks)) {
106
106
  this.config.stacks = iacConfig.stacks;
107
- this.logger.success(`Found ${this.config.stacks.length} stack(s) in configuration`);
108
107
  }
109
108
  // Save state
110
109
  this.saveMigrationState();
110
+ this.logger.success(`Configuration loaded (${this.config.stacks.length} stacks)`);
111
111
  }
112
112
  /**
113
113
  * Step 3: Create Safety Net PR (disable autodeploy for stacks)
114
114
  */
115
115
  async createSafetyNetPR() {
116
- this.logger.step(3, 9, 'Create Safety Net PR (disable autodeploy for stacks being migrated)');
116
+ this.logger.step(3, 9, 'Safety Net PR');
117
117
  // Skip if already created
118
118
  if (this.config.generatedFiles.safetyNetPR) {
119
119
  this.logger.success(`Safety net PR already exists: ${this.config.generatedFiles.safetyNetPR.url}`);
@@ -144,14 +144,21 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
144
144
  }
145
145
  // Create branch
146
146
  const branchName = this.generateBranchName(`safety-net-${this.config.serviceRepository.name}`);
147
+ if (this.dryRun) {
148
+ const stacks = iacConfig.stacks || [];
149
+ const stackNames = stacks.map(s => s.name).filter(Boolean);
150
+ this.logger.info(`[DRY RUN] Would create safety net PR`);
151
+ this.logger.info(` → branch: ${branchName}`);
152
+ this.logger.info(` → stacks: ${stackNames.join(', ')}`);
153
+ this.logger.success('Safety net PR step complete (dry run)');
154
+ return;
155
+ }
147
156
  await this.git.createBranch(branchName, this.config.repositories.iacDeploy);
148
157
  // Update all stacks to have autodeploy: false
149
158
  this.files.updateAutodeployInYaml(iacConfigPath, false);
150
159
  await this.git.add(['.'], this.config.repositories.iacDeploy);
151
160
  await this.git.commit(`chore: disable autodeploy for ${this.config.serviceRepository.name} stacks (migration safety net)`, this.config.repositories.iacDeploy);
152
- if (!this.dryRun) {
153
- this.logger.success(`Created safety net branch: ${branchName}`);
154
- }
161
+ this.logger.success(`Created safety net branch: ${branchName}`);
155
162
  // Push and create draft PR
156
163
  const prUrl = await this.git.createPR(branchName, `chore: disable autodeploy for ${this.config.serviceRepository.name} stacks (migration safety net)`, templates_1.PRTemplates.safetyNetPRBody(this.config.serviceRepository.name, this.config.jiraTicket), this.config.repositories.iacDeploy);
157
164
  this.config.generatedFiles.safetyNetPR = {
@@ -160,13 +167,12 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
160
167
  };
161
168
  this.saveMigrationState();
162
169
  this.logger.success(`Safety net PR created: ${prUrl}`);
163
- this.logger.warn(templates_1.MessageTemplates.SAFETY_NET_IMPORTANT);
164
170
  }
165
171
  /**
166
172
  * Step 4: Create Domain Enrollment PR
167
173
  */
168
174
  async createDomainEnrollmentPR() {
169
- this.logger.step(4, 9, 'Create Domain Enrollment PR (adds domain for service repository)');
175
+ this.logger.step(4, 9, 'Domain Enrollment PR');
170
176
  // Skip if already created
171
177
  if (this.config.generatedFiles.domainEnrollmentPR) {
172
178
  this.logger.success(`Domain enrollment PR already exists: ${this.config.generatedFiles.domainEnrollmentPR.url}`);
@@ -208,8 +214,14 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
208
214
  },
209
215
  },
210
216
  };
211
- this.files.writeYaml(spaceliftYamlPath, initialConfig);
212
- if (!this.dryRun) {
217
+ if (this.dryRun) {
218
+ this.logger.info('[DRY RUN] Would create new domain directory');
219
+ this.logger.info(` → domain: ${this.config.serviceRepository.domain}`);
220
+ this.logger.info(` → team: ${this.config.serviceRepository.team}`);
221
+ this.logger.info(` → okta: ${(this.config.serviceRepository.oktaGroups ?? []).join(', ')}`);
222
+ }
223
+ else {
224
+ this.files.writeYaml(spaceliftYamlPath, initialConfig);
213
225
  this.logger.success('Created domain directory and spacelift.yaml');
214
226
  }
215
227
  needsEnrollment = true;
@@ -242,8 +254,12 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
242
254
  autodeploy: false,
243
255
  };
244
256
  spaceliftConfig.metastacks = metastacks;
245
- this.files.writeYaml(spaceliftYamlPath, spaceliftConfig);
246
- if (!this.dryRun) {
257
+ if (this.dryRun) {
258
+ this.logger.info('[DRY RUN] Would add metastack to existing domain');
259
+ this.logger.info(` → domain: ${this.config.serviceRepository.domain}`);
260
+ }
261
+ else {
262
+ this.files.writeYaml(spaceliftYamlPath, spaceliftConfig);
247
263
  this.logger.success('Added repository to domain enrollment');
248
264
  }
249
265
  needsEnrollment = true;
@@ -252,23 +268,24 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
252
268
  // Only create branch and commit if there are changes
253
269
  if (needsEnrollment) {
254
270
  const branchName = this.generateBranchName(`enroll-${this.config.serviceRepository.name}`);
271
+ if (this.dryRun) {
272
+ this.logger.info('[DRY RUN] Would create domain enrollment PR');
273
+ this.logger.info(` → branch: ${branchName}`);
274
+ this.logger.success('Domain enrollment PR step complete (dry run)');
275
+ return;
276
+ }
255
277
  await this.git.createBranch(branchName, this.config.repositories.domainEnrollment);
256
278
  await this.git.add(['.'], this.config.repositories.domainEnrollment);
257
279
  await this.git.commit(`chore: enroll ${this.config.serviceRepository.name} in ${this.config.serviceRepository.domain} domain`, this.config.repositories.domainEnrollment);
258
- if (!this.dryRun) {
259
- this.logger.success(`Created branch: ${branchName}`);
260
- }
280
+ this.logger.success(`Created branch: ${branchName}`);
261
281
  // Push and create draft PR
262
- const prUrl = await this.git.createPR(branchName, `chore: enroll ${this.config.serviceRepository.name} in ${this.config.serviceRepository.domain} domain`, templates_1.PRTemplates.domainEnrollmentPRBody(this.config.serviceRepository.name, this.config.serviceRepository.domain, this.config.jiraTicket), this.config.repositories.domainEnrollment);
282
+ const prUrl = await this.git.createPR(branchName, `chore: enroll ${this.config.serviceRepository.name} in ${this.config.serviceRepository.domain} domain`, templates_1.PRTemplates.domainEnrollmentPRBody(this.config.serviceRepository.name, this.config.serviceRepository.domain, this.config.jiraTicket, this.config.generatedFiles.safetyNetPR?.url), this.config.repositories.domainEnrollment);
263
283
  this.config.generatedFiles.domainEnrollmentPR = {
264
284
  branch: branchName,
265
285
  url: prUrl,
266
286
  };
267
287
  this.saveMigrationState();
268
- this.logger.info('\nNext steps for domain enrollment:');
269
- this.logger.info(`1. Review PR: ${prUrl}`);
270
- this.logger.info(`2. Get it reviewed and approved`);
271
- this.logger.info('3. Merge PR to deploy repository metastack');
288
+ this.logger.info(`Next: Review, approve and merge the PR: ${prUrl}`);
272
289
  const shouldContinue = await this.confirm('\nHave you merged the domain enrollment PR?', false);
273
290
  if (!shouldContinue) {
274
291
  this.exitWithMessage('Please merge the PR before continuing');
@@ -291,13 +308,13 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
291
308
  }
292
309
  }
293
310
  /**
294
- * Step 5: Create Service Repository PR (spacelift.yaml, import.tf)
311
+ * Step 6: Create Service Repository PR (spacelift.yaml, import.tf)
295
312
  */
296
313
  async createServiceRepositoryPR() {
297
- this.logger.step(5, 9, 'Create Service Repository PR (spacelift.yaml, .metastack/import.tf)');
314
+ this.logger.step(6, 9, 'Service Repository PR');
298
315
  // Create safety net PR first if not already created
299
316
  if (!this.config.generatedFiles.safetyNetPR) {
300
- this.logger.info('\nSafety net PR not yet created, creating it first...');
317
+ this.logger.info('Creating safety net PR first...');
301
318
  await this.createSafetyNetPR();
302
319
  }
303
320
  // Skip if already created
@@ -315,8 +332,11 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
315
332
  // Copy spacelift.yaml from iac-deploy
316
333
  const iacConfigPath = this.paths.iacConfigFile(this.config.serviceRepository.name);
317
334
  const targetSpacelliftPath = this.paths.serviceRepoSpacelliftYaml();
318
- this.files.copyFile(iacConfigPath, targetSpacelliftPath);
319
- if (!this.dryRun) {
335
+ if (this.dryRun) {
336
+ this.logger.info('[DRY RUN] Would copy spacelift.yaml from iac-deploy');
337
+ }
338
+ else {
339
+ this.files.copyFile(iacConfigPath, targetSpacelliftPath);
320
340
  this.logger.success('Created spacelift.yaml');
321
341
  }
322
342
  // Create .metastack/import.tf
@@ -329,30 +349,41 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
329
349
  }
330
350
  // Only add if not already present
331
351
  if (!gitignoreContent.includes('.migration-tmp/')) {
332
- gitignoreContent += constants_1.GITIGNORE_ENTRIES + '\n';
333
- this.files.writeFile(gitignorePath, gitignoreContent);
334
- if (!this.dryRun) {
352
+ if (this.dryRun) {
353
+ this.logger.info('[DRY RUN] Would update .gitignore');
354
+ }
355
+ else {
356
+ gitignoreContent += constants_1.GITIGNORE_ENTRIES + '\n';
357
+ this.files.writeFile(gitignorePath, gitignoreContent);
335
358
  this.logger.success('Updated .gitignore');
336
359
  }
337
360
  }
338
361
  // Create branch and commit
339
362
  const branchName = this.generateBranchName('metastack-migration');
363
+ if (this.dryRun) {
364
+ this.logger.info('[DRY RUN] Would create service repository PR');
365
+ this.logger.info(` → branch: ${branchName}`);
366
+ this.logger.info(` → files: spacelift.yaml, .metastack/, .gitignore`);
367
+ this.logger.success('Service repository PR step complete (dry run)');
368
+ return;
369
+ }
340
370
  await this.git.createBranch(branchName, this.config.serviceRepository.path);
341
371
  await this.git.add(['spacelift.yaml', '.metastack/', '.gitignore'], this.config.serviceRepository.path);
342
372
  await this.git.commit(`chore: migrate to metastack\n\nAdd spacelift.yaml and import.tf for metastack migration`, this.config.serviceRepository.path);
343
- if (!this.dryRun) {
344
- this.logger.success(`Created branch: ${branchName}`);
345
- }
373
+ this.logger.success(`Created branch: ${branchName}`);
346
374
  // Push and create draft PR
347
- const prUrl = await this.git.createPR(branchName, `chore: migrate to metastack`, templates_1.PRTemplates.serviceRepoPRBody(this.config.serviceRepository.name, this.config.jiraTicket), this.config.serviceRepository.path);
375
+ const prUrl = await this.git.createPR(branchName, `chore: migrate to metastack`, templates_1.PRTemplates.serviceRepoPRBody(this.config.serviceRepository.name, this.config.jiraTicket, {
376
+ safetyNet: this.config.generatedFiles.safetyNetPR?.url,
377
+ domainEnrollment: this.config.generatedFiles.domainEnrollmentPR?.url,
378
+ iacDeployCleanup: this.config.generatedFiles.iacDeployPR?.url,
379
+ }), this.config.serviceRepository.path);
348
380
  this.config.generatedFiles.serviceRepoPR = {
349
381
  branch: branchName,
350
382
  url: prUrl,
351
383
  };
352
384
  this.saveMigrationState();
385
+ this.logger.success(`Service repository PR created: ${prUrl}`);
353
386
  this.logger.warn(templates_1.MessageTemplates.CLEANUP_PR_NOT_MERGE_WARNING);
354
- this.logger.info(templates_1.MessageTemplates.CLEANUP_PR_MERGE_TIMING);
355
- this.logger.info(`PR URL: ${prUrl}`);
356
387
  }
357
388
  /**
358
389
  * Generate import.tf file with import blocks for stacks
@@ -362,12 +393,12 @@ class MigrationTool extends migration_helper_1.MigrationHelper {
362
393
  this.files.mkdir(metastackDir, { recursive: true });
363
394
  // Ensure spacectl is authenticated before fetching resources
364
395
  await this.ensureSpacectlAuthentication();
365
- this.logger.info('Fetching stack resources from projects-manager...');
396
+ this.logger.info('Fetching stack resources...');
366
397
  // Fetch all resources from projects-manager
367
398
  let projectManagerResources = '';
368
399
  try {
369
400
  projectManagerResources = this.getStackResourcesFromDirectory(constants_1.STACK_MANAGERS.projectsManager, this.config.repositories.iacDeploy);
370
- this.logger.success('Fetched resources from projects-manager');
401
+ this.logger.success('Fetched resources');
371
402
  }
372
403
  catch (error) {
373
404
  this.logger.error(`Failed to fetch resources from projects-manager: ${error}`);
@@ -456,10 +487,17 @@ import {
456
487
  }
457
488
  }
458
489
  const importPath = this.paths.importTfFile();
459
- this.files.writeFile(importPath, importTf);
460
- if (!this.dryRun) {
461
- this.logger.success('Created .metastack/import.tf with import blocks');
462
- this.logger.info(` Total stacks: ${this.config.stacks.length}`);
490
+ if (this.dryRun) {
491
+ const stackIds = this.config.stacks.map(s => s.identifier).slice(0, 5);
492
+ const more = this.config.stacks.length > 5
493
+ ? ` (+${this.config.stacks.length - 5} more)`
494
+ : '';
495
+ this.logger.info(`[DRY RUN] Would create import.tf (${this.config.stacks.length} stacks)`);
496
+ this.logger.info(` → stacks: ${stackIds.join(', ')}${more}`);
497
+ }
498
+ else {
499
+ this.files.writeFile(importPath, importTf);
500
+ this.logger.success(`Created .metastack/import.tf (${this.config.stacks.length} stacks)`);
463
501
  }
464
502
  // Update version.tf with Spacelift provider
465
503
  const versionTfPath = path.join(metastackDir, 'version.tf');
@@ -472,20 +510,23 @@ import {
472
510
  const existingContent = this.files.readFile(versionTfPath);
473
511
  // Check if spacelift provider is already configured
474
512
  if (!existingContent.includes('spacelift')) {
475
- this.logger.info('Updating version.tf to include Spacelift provider...');
513
+ this.logger.info('Adding Spacelift provider to version.tf...');
476
514
  const updatedContent = this.addSpaceliftProviderToVersionTf(existingContent);
477
515
  if (updatedContent !== existingContent) {
478
- this.files.writeFile(versionTfPath, updatedContent);
479
- if (!this.dryRun) {
480
- this.logger.success('Updated version.tf with Spacelift provider');
516
+ if (this.dryRun) {
517
+ this.logger.info('[DRY RUN] Would add Spacelift provider to version.tf');
518
+ }
519
+ else {
520
+ this.files.writeFile(versionTfPath, updatedContent);
521
+ this.logger.success('Added Spacelift provider');
481
522
  }
482
523
  }
483
524
  else {
484
- this.logger.warn('Could not update version.tf automatically - please add Spacelift provider manually');
525
+ this.logger.warn('Could not update version.tf, you should add the Spacelift provider manually');
485
526
  }
486
527
  }
487
528
  else {
488
- this.logger.info('version.tf already contains Spacelift provider');
529
+ this.logger.info('Spacelift provider already in version.tf');
489
530
  }
490
531
  }
491
532
  /**
@@ -594,53 +635,61 @@ $2`);
594
635
  return { contexts, dependencies };
595
636
  }
596
637
  /**
597
- * Step 6: Create Cleanup PR for iac-deploy
638
+ * Step 5: Create iac-deploy Cleanup PR
598
639
  */
599
- async createCleanupPRs() {
600
- this.logger.step(6, 9, 'Create Cleanup PR for iac-deploy (remove old resources)');
640
+ async createIacDeployCleanupPR() {
641
+ this.logger.step(5, 9, 'iac-deploy Cleanup PR');
601
642
  // Ask for confirmation before creating cleanup PR
602
643
  const shouldCreate = await this.confirm('\nCreate and push cleanup PR (iac-deploy)?', true);
603
644
  if (!shouldCreate) {
604
645
  this.logger.warn('Skipping cleanup PR creation');
605
646
  return;
606
647
  }
607
- await this.createIacDeployCleanupPR();
648
+ await this.doCreateIacDeployCleanupPR();
608
649
  await this.generateMigrationProcedure();
650
+ this.logger.success('iac-deploy Cleanup PR step complete');
609
651
  this.logger.warn(templates_1.MessageTemplates.CLEANUP_PR_NOT_MERGE_WARNING);
610
- this.logger.info(templates_1.MessageTemplates.CLEANUP_PR_STATE_REMOVAL_WARNING);
611
652
  }
612
653
  /**
613
- * Create cleanup PR for iac-deploy
654
+ * Create cleanup PR for iac-deploy (internal)
614
655
  */
615
- async createIacDeployCleanupPR() {
616
- this.logger.info('\nCreating iac-deploy cleanup PR...');
656
+ async doCreateIacDeployCleanupPR() {
657
+ this.logger.info('Creating iac-deploy cleanup PR...');
617
658
  const configPath = this.paths.iacConfigFile(this.config.serviceRepository.name);
618
659
  // Remove the config file
619
660
  const shouldDelete = await this.confirm(`Delete ${this.config.serviceRepository.name}.yaml from iac-deploy?`, true);
620
661
  if (shouldDelete) {
621
662
  const branchName = this.generateBranchName(`cleanup-${this.config.serviceRepository.name}`);
663
+ if (this.dryRun) {
664
+ this.logger.info('[DRY RUN] Would create iac-deploy cleanup PR');
665
+ this.logger.info(` → branch: ${branchName}`);
666
+ this.logger.info(` → removes: ${this.config.serviceRepository.name}.yaml`);
667
+ this.logger.success('iac-deploy cleanup PR step complete (dry run)');
668
+ return;
669
+ }
622
670
  await this.git.createBranch(branchName, this.config.repositories.iacDeploy);
623
671
  this.files.deleteFile(configPath);
624
672
  await this.git.add(['.'], this.config.repositories.iacDeploy);
625
673
  await this.git.commit(`chore: remove ${this.config.serviceRepository.name} config (migrated to metastack)`, this.config.repositories.iacDeploy);
626
- if (!this.dryRun) {
627
- this.logger.success(`Created iac-deploy cleanup branch: ${branchName}`);
628
- }
674
+ this.logger.success(`Created iac-deploy cleanup branch: ${branchName}`);
629
675
  // Push and create draft PR
630
- const prUrl = await this.git.createPR(branchName, `chore: remove ${this.config.serviceRepository.name} config (migrated to metastack)`, templates_1.PRTemplates.iacDeployCleanupPRBody(this.config.serviceRepository.name, this.config.jiraTicket), this.config.repositories.iacDeploy);
676
+ const prUrl = await this.git.createPR(branchName, `chore: remove ${this.config.serviceRepository.name} config (migrated to metastack)`, templates_1.PRTemplates.iacDeployCleanupPRBody(this.config.serviceRepository.name, this.config.jiraTicket, {
677
+ safetyNet: this.config.generatedFiles.safetyNetPR?.url,
678
+ domainEnrollment: this.config.generatedFiles.domainEnrollmentPR?.url,
679
+ }), this.config.repositories.iacDeploy);
631
680
  this.config.generatedFiles.iacDeployPR = {
632
681
  branch: branchName,
633
682
  url: prUrl,
634
683
  };
635
684
  this.saveMigrationState();
636
- this.logger.info(`PR URL: ${prUrl}`);
685
+ this.logger.success(`iac-deploy cleanup PR created: ${prUrl}`);
637
686
  }
638
687
  }
639
688
  /**
640
689
  * Step 7: Generate State Removal Commands
641
690
  */
642
691
  async generateStateRemovalCommands() {
643
- this.logger.step(7, 9, 'Generate State Removal Commands (spacectl stacks task)');
692
+ this.logger.step(7, 9, 'State Removal Commands');
644
693
  let commands = `# State Removal Commands
645
694
  # These commands must be run BEFORE merging cleanup PRs
646
695
  # They use spacectl to safely remove resources from Terraform state
@@ -666,24 +715,18 @@ $2`);
666
715
  }
667
716
  const commandsPath = this.paths.stateRemovalCommands;
668
717
  this.files.writeFile(commandsPath, commands);
669
- if (!this.dryRun) {
670
- this.logger.success(`Generated state removal commands: ${commandsPath}`);
671
- this.config.generatedFiles.stateRemovalCommands = commandsPath;
672
- // Save state after generating commands
673
- this.saveMigrationState();
674
- this.logger.info(templates_1.MessageTemplates.STATE_REMOVAL_REVIEW_WARNING);
675
- }
718
+ this.config.generatedFiles.stateRemovalCommands = commandsPath;
719
+ this.saveMigrationState();
720
+ this.logger.success(`State removal commands generated: ${commandsPath}`);
721
+ this.logger.warn(templates_1.MessageTemplates.STATE_REMOVAL_REVIEW_WARNING);
676
722
  }
677
723
  /**
678
724
  * Step 8: Update Stack Spaces
679
725
  */
680
726
  async updateStackSpaces() {
681
- this.logger.step(8, 9, 'Update Stack Spaces (Move stacks to domain space)');
727
+ this.logger.step(8, 9, 'Update Stack Spaces to domain space');
682
728
  this.logger.info(templates_1.MessageTemplates.STACK_SPACES_DESCRIPTION);
683
- this.logger.info(templates_1.MessageTemplates.STACK_SPACES_REASON);
684
- this.logger.info('');
685
729
  this.logger.warn(templates_1.MessageTemplates.STACK_SPACES_IMPORTANT_TIMING);
686
- this.logger.info('');
687
730
  const shouldUpdate = await this.confirm('Update stack spaces now?', false);
688
731
  if (!shouldUpdate) {
689
732
  this.logger.warn('Skipping stack space update');
@@ -696,26 +739,24 @@ $2`);
696
739
  const targetSpaceName = `domain-${this.config.serviceRepository.domain}`;
697
740
  this.logger.info(`Target space: ${targetSpaceName}`);
698
741
  if (this.dryRun) {
699
- this.logger.info('\n[DRY RUN] Would update stack spaces:');
700
- this.logger.info(` Target space: ${targetSpaceName}`);
701
- this.logger.info(` Stacks to update: ${this.config.stacks.length}`);
702
- for (const stack of this.config.stacks) {
703
- const stackId = this.getStackModuleKey(stack);
704
- this.logger.info(` - ${stackId}`);
705
- }
742
+ const stackIds = this.config.stacks.map(s => s.identifier).slice(0, 5);
743
+ const more = this.config.stacks.length > 5
744
+ ? ` (+${this.config.stacks.length - 5} more)`
745
+ : '';
746
+ this.logger.info(`[DRY RUN] Would move ${this.config.stacks.length} stacks to ${targetSpaceName}`);
747
+ this.logger.info(` stacks: ${stackIds.join(', ')}${more}`);
748
+ this.logger.success('Migrate spaces step complete (dry run)');
706
749
  return;
707
750
  }
708
751
  // Initialize GraphQL client
709
752
  const client = new spacelift_client_1.SpaceliftGraphQLClient();
710
- // Get the target space ID
711
- this.logger.info(`\nLooking up space ID for "${targetSpaceName}"...`);
753
+ this.logger.info(`Looking up space ID for ${targetSpaceName}...`);
712
754
  const targetSpaceId = await client.getSpaceByPath(targetSpaceName);
713
755
  if (!targetSpaceId) {
714
756
  throw new Error(`Space "${targetSpaceName}" not found. Please ensure the domain enrollment PR has been merged and the space has been created.`);
715
757
  }
716
758
  this.logger.success(`Found target space ID: ${targetSpaceId}`);
717
- // Update each stack
718
- this.logger.info(`\nUpdating ${this.config.stacks.length} stacks...`);
759
+ this.logger.info(`Updating ${this.config.stacks.length} stacks...`);
719
760
  const results = [];
720
761
  for (const stack of this.config.stacks) {
721
762
  const stackId = this.getStackModuleKey(stack);
@@ -758,35 +799,35 @@ $2`);
758
799
  const successful = results.filter(r => r.success).length;
759
800
  const alreadyInTarget = results.filter(r => r.alreadyInTargetSpace).length;
760
801
  const failed = results.filter(r => !r.success).length;
761
- this.logger.info('\nStack space update summary:');
762
- this.logger.info(` Successful updates: ${successful - alreadyInTarget}`);
763
- this.logger.info(` Already in target space: ${alreadyInTarget}`);
802
+ this.logger.info(`Summary: ${successful - alreadyInTarget} updated, ${alreadyInTarget} already in place`);
764
803
  if (failed > 0) {
765
- this.logger.error(` Failed: ${failed}`);
766
- this.logger.info('\nFailed stacks:');
804
+ this.logger.error(`Failed: ${failed}`);
767
805
  results
768
806
  .filter(r => !r.success)
769
- .forEach(r => {
770
- this.logger.error(` - ${r.stackId}: ${r.error}`);
771
- });
772
- throw new Error(`Failed to update ${failed} stack(s). Please review the errors above.`);
807
+ .forEach(r => this.logger.error(` ${r.stackId}: ${r.error}`));
808
+ throw new Error(`Failed to update ${failed} stack(s)`);
773
809
  }
774
- this.logger.success('\nAll stacks successfully updated to target space');
810
+ this.logger.success('All stacks updated');
775
811
  }
776
812
  /**
777
813
  * Step 9: Generate Migration Procedure
778
814
  */
779
815
  async generateMigrationProcedure() {
780
- this.logger.step(9, 9, 'Generate Migration Procedure Documentation');
816
+ this.logger.step(9, 9, 'Generate Procedure doc');
781
817
  const procedure = (0, templates_1.generateMigrationProcedure)(this.config);
782
818
  const procedurePath = this.paths.procedure;
783
- this.files.writeFile(procedurePath, procedure);
784
- if (!this.dryRun) {
785
- this.logger.success(`Generated migration procedure: ${procedurePath}`);
786
- this.config.generatedFiles.migrationProcedure = procedurePath;
787
- // Save final state
788
- this.saveMigrationState();
819
+ if (this.dryRun) {
820
+ this.logger.info('[DRY RUN] Would generate migration procedure');
821
+ this.logger.info(` path: ${procedurePath}`);
822
+ this.logger.info(` → domain: ${this.config.serviceRepository.domain}`);
823
+ this.logger.info(` → stacks: ${this.config.stacks.length}`);
824
+ this.logger.success('Generate procedure step complete (dry run)');
825
+ return;
789
826
  }
827
+ this.files.writeFile(procedurePath, procedure);
828
+ this.logger.success(`Generated migration procedure: ${procedurePath}`);
829
+ this.config.generatedFiles.migrationProcedure = procedurePath;
830
+ this.saveMigrationState();
790
831
  }
791
832
  /**
792
833
  * Display migration state
@@ -844,21 +885,65 @@ $2`);
844
885
  }
845
886
  }
846
887
  /**
847
- * Clean up migration files
888
+ * Clean up migration (creates PR + removes local files)
848
889
  */
849
890
  async cleanupMigration() {
850
891
  this.logger.section('Migration Cleanup');
851
- this.logger.info('This will remove all migration-related files:');
852
- this.logger.info(` - ${this.paths.tmpDir} (cloned repositories)`);
853
- this.logger.info(` - ${this.paths.migrationState} (migration state)`);
854
- this.logger.info(` - ${this.paths.stateRemovalCommands}`);
855
- this.logger.info(` - ${this.paths.procedure}`);
892
+ this.logger.warn('⚠️ Only run this AFTER all migration PRs are merged and resources are imported!');
893
+ this.logger.info('');
894
+ this.logger.info('This will:');
895
+ this.logger.info(' 1. Create a PR to remove import.tf and spacelift provider');
896
+ this.logger.info(' 2. Remove local temp files (.migration-tmp, state, scripts)');
856
897
  this.logger.info('');
857
- const shouldCleanup = await this.confirm('Remove all migration files?', false);
898
+ const shouldCleanup = await this.confirm('Proceed with cleanup?', true);
858
899
  if (!shouldCleanup) {
859
900
  this.logger.info('Cleanup cancelled');
860
901
  return;
861
902
  }
903
+ // --- Part 1: Create cleanup PR ---
904
+ const importTfPath = this.paths.importTfFile();
905
+ const versionTfPath = path.join(this.paths.metastackDir, 'version.tf');
906
+ const branchName = this.generateBranchName('cleanup');
907
+ const canCreatePR = this.files.exists(importTfPath) && this.files.exists(versionTfPath);
908
+ if (canCreatePR) {
909
+ if (this.dryRun) {
910
+ this.logger.info('[DRY RUN] Would create cleanup PR');
911
+ this.logger.info(` → branch: ${branchName}`);
912
+ this.logger.info(` → removes: import.tf, spacelift provider`);
913
+ }
914
+ else {
915
+ await this.git.createBranch(branchName, this.config.serviceRepository.path);
916
+ // Remove import.tf
917
+ this.files.deleteFile(importTfPath);
918
+ this.logger.success('Deleted .metastack/import.tf');
919
+ // Remove spacelift provider from version.tf
920
+ const versionTfContent = this.files.readFile(versionTfPath);
921
+ const updatedVersionTf = this.removeSpaceliftProviderFromVersionTf(versionTfContent);
922
+ if (updatedVersionTf !== versionTfContent) {
923
+ this.files.writeFile(versionTfPath, updatedVersionTf);
924
+ this.logger.success('Removed Spacelift provider from version.tf');
925
+ }
926
+ // Commit and create PR
927
+ await this.git.add(['.metastack/'], this.config.serviceRepository.path);
928
+ await this.git.commit(`chore: remove migration files (import.tf and spacelift provider)`, this.config.serviceRepository.path);
929
+ const prUrl = await this.git.createPR(branchName, 'chore: remove migration files (import.tf and spacelift provider)', templates_1.PRTemplates.postMigrationCleanupPRBody(this.config.serviceRepository.name, this.config.jiraTicket), this.config.serviceRepository.path);
930
+ this.logger.success(`Cleanup PR created: ${prUrl}`);
931
+ }
932
+ }
933
+ else {
934
+ this.logger.info('Skipping PR creation (import.tf already removed)');
935
+ }
936
+ // --- Part 2: Remove local temp files ---
937
+ this.logger.info('');
938
+ if (this.dryRun) {
939
+ this.logger.info('[DRY RUN] Would remove local temp files');
940
+ this.logger.info(` → ${this.paths.tmpDir}`);
941
+ this.logger.info(` → ${this.paths.migrationState}`);
942
+ this.logger.info(` → ${this.paths.stateRemovalCommands}`);
943
+ this.logger.info(` → ${this.paths.procedure}`);
944
+ this.logger.success('Cleanup step complete (dry run)');
945
+ return;
946
+ }
862
947
  const filesToRemove = [
863
948
  this.paths.tmpDir,
864
949
  this.paths.migrationState,
@@ -869,82 +954,15 @@ $2`);
869
954
  if (this.files.exists(file)) {
870
955
  if (this.files.isDirectory(file)) {
871
956
  this.files.deleteDirectory(file);
872
- this.logger.success(`Removed directory: ${path.basename(file)}`);
957
+ this.logger.success(`Removed: ${path.basename(file)}`);
873
958
  }
874
959
  else {
875
960
  this.files.deleteFile(file);
876
- this.logger.success(`Removed file: ${path.basename(file)}`);
961
+ this.logger.success(`Removed: ${path.basename(file)}`);
877
962
  }
878
963
  }
879
964
  }
880
- this.logger.info('');
881
- const shouldRemoveScript = await this.confirm('Remove the migration script (migrate-to-metastack.ts)?', false);
882
- if (shouldRemoveScript) {
883
- const scriptPath = path.join(this.config.serviceRepository.path, 'migrate-to-metastack.ts');
884
- this.files.deleteFile(scriptPath);
885
- this.logger.success('Removed migrate-to-metastack.ts');
886
- }
887
965
  this.logger.success('\nCleanup complete!');
888
- this.logger.info('Migration files have been removed');
889
- }
890
- /**
891
- * Create Final Cleanup PR (remove import.tf and spacelift provider from version.tf)
892
- */
893
- async createFinalCleanupPR() {
894
- this.logger.section('Final Cleanup PR');
895
- this.logger.info('This command creates a PR to remove temporary migration files:');
896
- this.logger.info(' - .metastack/import.tf');
897
- this.logger.info(' - Spacelift provider from .metastack/version.tf');
898
- this.logger.info('');
899
- this.logger.warn('⚠️ Only run this AFTER all migration PRs are merged and resources are imported!');
900
- this.logger.info('');
901
- const shouldCreate = await this.confirm('Create final cleanup PR on service repository?', true);
902
- if (!shouldCreate) {
903
- this.logger.warn('Skipping final cleanup PR creation');
904
- return;
905
- }
906
- const importTfPath = this.paths.importTfFile();
907
- const versionTfPath = path.join(this.paths.metastackDir, 'version.tf');
908
- // Check if files exist
909
- if (!this.files.exists(importTfPath)) {
910
- this.logger.warn(`import.tf not found at ${importTfPath} - it may have already been removed`);
911
- return;
912
- }
913
- if (!this.files.exists(versionTfPath)) {
914
- this.logger.error(`version.tf not found at ${versionTfPath}`);
915
- return;
916
- }
917
- // Create branch
918
- const branchName = this.generateBranchName('final-cleanup');
919
- await this.git.createBranch(branchName, this.config.serviceRepository.path);
920
- // Remove import.tf
921
- this.files.deleteFile(importTfPath);
922
- if (!this.dryRun) {
923
- this.logger.success('Deleted .metastack/import.tf');
924
- }
925
- // Remove spacelift provider from version.tf
926
- const versionTfContent = this.files.readFile(versionTfPath);
927
- const updatedVersionTf = this.removeSpaceliftProviderFromVersionTf(versionTfContent);
928
- if (updatedVersionTf !== versionTfContent) {
929
- this.files.writeFile(versionTfPath, updatedVersionTf);
930
- if (!this.dryRun) {
931
- this.logger.success('Removed Spacelift provider from version.tf');
932
- }
933
- }
934
- else {
935
- this.logger.warn('Spacelift provider not found in version.tf or could not be removed');
936
- }
937
- // Commit changes
938
- await this.git.add(['.metastack/'], this.config.serviceRepository.path);
939
- await this.git.commit(`chore: remove migration files (import.tf and spacelift provider)\n\nRemove temporary migration files after successful import`, this.config.serviceRepository.path);
940
- if (!this.dryRun) {
941
- this.logger.success(`Created branch: ${branchName}`);
942
- }
943
- // Push and create draft PR
944
- const prUrl = await this.git.createPR(branchName, 'chore: remove migration files (import.tf and spacelift provider)', templates_1.PRTemplates.finalCleanupPRBody(this.config.serviceRepository.name, this.config.jiraTicket), this.config.serviceRepository.path);
945
- this.logger.success(`Final cleanup PR created: ${prUrl}`);
946
- this.logger.info('');
947
- this.logger.warn('⚠️ Only merge this PR after confirming all resources are imported!');
948
966
  }
949
967
  /**
950
968
  * Full migration workflow
@@ -952,10 +970,8 @@ $2`);
952
970
  async run() {
953
971
  this.logger.section('Metastack Migration Tool');
954
972
  this.logger.info('This tool will guide you through the metastack migration');
955
- this.logger.info('');
956
973
  if (this.dryRun) {
957
974
  this.logger.warn(templates_1.MessageTemplates.DRY_RUN_MODE_WARNING);
958
- this.logger.info('');
959
975
  }
960
976
  // Load existing migration state if available
961
977
  const hasExistingState = this.loadMigrationState();
@@ -967,42 +983,28 @@ $2`);
967
983
  await this.loadConfiguration();
968
984
  await this.createSafetyNetPR();
969
985
  await this.createDomainEnrollmentPR();
986
+ await this.createIacDeployCleanupPR();
970
987
  await this.createServiceRepositoryPR();
971
- await this.createCleanupPRs();
972
988
  await this.generateStateRemovalCommands();
973
989
  await this.generateMigrationProcedure();
974
990
  this.logger.section('Migration Setup Complete!');
975
- this.logger.success('All preparation complete');
976
991
  this.logger.info('');
977
992
  // Display PR URLs
978
993
  this.logger.info('Created Pull Requests:');
979
994
  if (this.config.generatedFiles.safetyNetPR) {
980
- this.logger.info(` Safety Net (autodeploy=false): ${this.config.generatedFiles.safetyNetPR.url}`);
995
+ this.logger.info(` 1. Safety Net: ${this.config.generatedFiles.safetyNetPR.url}`);
981
996
  }
982
997
  if (this.config.generatedFiles.domainEnrollmentPR) {
983
- this.logger.info(` Domain Enrollment: ${this.config.generatedFiles.domainEnrollmentPR.url}`);
984
- }
985
- if (this.config.generatedFiles.serviceRepoPR) {
986
- this.logger.info(` Service Repository: ${this.config.generatedFiles.serviceRepoPR.url}`);
998
+ this.logger.info(` 2. Domain Enrollment: ${this.config.generatedFiles.domainEnrollmentPR.url}`);
987
999
  }
988
1000
  if (this.config.generatedFiles.iacDeployPR) {
989
- this.logger.info(` iac-deploy Cleanup: ${this.config.generatedFiles.iacDeployPR.url}`);
1001
+ this.logger.info(` 3. iac-deploy Cleanup: ${this.config.generatedFiles.iacDeployPR.url}`);
1002
+ }
1003
+ if (this.config.generatedFiles.serviceRepoPR) {
1004
+ this.logger.info(` 4. Service Repository: ${this.config.generatedFiles.serviceRepoPR.url}`);
990
1005
  }
991
1006
  this.logger.info('');
992
- this.logger.info('Generated files:');
993
- this.logger.info(` - ${this.config.generatedFiles.migrationProcedure}`);
994
- this.logger.info(` - ${this.config.generatedFiles.stateRemovalCommands}`);
995
- this.logger.info('');
996
- this.logger.warn(templates_1.MessageTemplates.FINAL_WORKFLOW_NEXT_STEPS);
997
- this.logger.info('1. Review the PRs above');
998
- this.logger.info('2. Review METASTACK_MIGRATION_PROCEDURE.md');
999
- this.logger.info('3. Execute state removal commands from METASTACK_STATE_REMOVAL_COMMANDS.sh');
1000
- this.logger.info('4. Run migrate-spaces command manually: nx run <project>:stack-migration --command=migrate-spaces');
1001
- this.logger.info('5. Follow the migration procedure to merge PRs in correct order');
1002
- this.logger.info('');
1003
- this.logger.warn(templates_1.MessageTemplates.FINAL_WORKFLOW_EXECUTE_ORDER_WARNING);
1004
- this.logger.info('');
1005
- this.logger.info('Note: Migration state saved to .migration-state.json for resumability');
1007
+ this.logger.info('Follow the migration procedure in METASTACK_MIGRATION_PROCEDURE.md');
1006
1008
  }
1007
1009
  }
1008
1010
  exports.MigrationTool = MigrationTool;