diff --git a/packages/amplify-gen2-migration-e2e-system/src/__tests__/amplify-initializer.e2e.test.ts b/packages/amplify-gen2-migration-e2e-system/src/__tests__/amplify-initializer.e2e.test.ts index 2d02ad3b096..d0344540498 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/__tests__/amplify-initializer.e2e.test.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/__tests__/amplify-initializer.e2e.test.ts @@ -274,7 +274,7 @@ describe('AmplifyInitializer E2E', () => { console.log('✅ Amplify CLI is available, proceeding with test'); // Generate a unique alphanumeric app name (3-20 chars, alphanumeric only) - const appName = generateTimeBasedE2EAmplifyAppName(); + const appName = generateTimeBasedE2EAmplifyAppName('app-name'); const profile = TEST_RUNNER_PROFILE; const config = { diff --git a/packages/amplify-gen2-migration-e2e-system/src/__tests__/configuration-loader.test.ts b/packages/amplify-gen2-migration-e2e-system/src/__tests__/configuration-loader.test.ts index 5ac04bc7768..b23c3b7a649 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/__tests__/configuration-loader.test.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/__tests__/configuration-loader.test.ts @@ -52,9 +52,6 @@ describe('ConfigurationLoader', () => { }, ], }, - hosting: { - type: 'amplify-console', - }, }, }; diff --git a/packages/amplify-gen2-migration-e2e-system/src/cli.ts b/packages/amplify-gen2-migration-e2e-system/src/cli.ts index ddbe8c58dca..ab905c5ca28 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/cli.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/cli.ts @@ -149,6 +149,14 @@ async function main(): Promise { envName: argv.envName, }; + // Select apps to process + logger.debug('Selecting apps for migration...'); + await appSelector.validateAppExists(options.app); + const selectedApp = options.app; + const deploymentName = generateTimeBasedE2EAmplifyAppName(selectedApp); + + logger.setAppName(deploymentName); + // Detect environment and get credentials if needed logger.debug('Detecting execution environment...'); let environment; @@ -164,10 +172,6 @@ async function main(): Promise { logger.info('Atmosphere environment validated successfully'); } } - const environmentSummary = environmentDetector.getEnvironmentSummary(); - - logger.debug(`Environment: ${environment}`); - logger.debug('Environment details:', environmentSummary); // Get appropriate credentials based on environment let profile: string; @@ -190,11 +194,6 @@ async function main(): Promise { profile = options.profile!; } - // Select apps to process - logger.debug('Selecting apps for migration...'); - const selectedApp = await appSelector.selectApp(options); - const deploymentName = generateTimeBasedE2EAmplifyAppName(selectedApp); - // Generate envName if not provided via CLI const envName = options.envName ?? AmplifyInitializer.generateRandomEnvName(); logger.info(`Using Amplify environment name: ${envName}`); @@ -391,33 +390,13 @@ async function runGen1TestScript(targetAppPath: string, migrationTargetPath: str logger.info(`Running ${testScriptName} in ${targetAppPath}`); const result = await execa('npx', ['tsx', testScriptName], { cwd: targetAppPath, + stdio: 'inherit', reject: false, env: { ...process.env, AWS_SDK_LOAD_CONFIG: '1' }, }); - const stdout = result.stdout || ''; - const stderr = result.stderr || ''; - - // Partition stdout into test-result summary lines (INFO) and everything else (DEBUG) - const testResultLines = stdout.split('\n').filter((line) => { - const trimmed = line.trim(); - return ( - trimmed.startsWith('✅') || - trimmed.startsWith('❌') || - trimmed.includes('TEST SUMMARY') || - trimmed.includes('All tests passed') || - trimmed.includes('test(s) failed') - ); - }); - - for (const line of testResultLines) { - logger.info(`[test-script] ${line.trim()}`); - } - if (result.exitCode !== 0) { - // Include output in the error so it's visible even without --verbose - const combinedOutput = [stdout, stderr].filter(Boolean).join('\n'); - throw new Error(`${testScriptName} failed with exit code ${result.exitCode}\n${combinedOutput}`); + throw new Error(`${testScriptName} failed with exit code ${result.exitCode}\n`); } logger.info(`${testScriptName} completed successfully`); @@ -444,10 +423,6 @@ async function runGen2TestScript(targetAppPath: string, migrationTargetPath: str logger.info(`Copying _test-common to ${testCommonDest}`); await fsExtra.copy(testCommonSource, testCommonDest, { overwrite: true }); - // Install dependencies for the test script - logger.info(`Installing dependencies in ${targetAppPath}`); - await execa('npm', ['install'], { cwd: targetAppPath }); - // Install dependencies for _test-common logger.info(`Installing _test-common dependencies in ${testCommonDest}`); await execa('npm', ['install'], { cwd: testCommonDest }); @@ -455,37 +430,12 @@ async function runGen2TestScript(targetAppPath: string, migrationTargetPath: str logger.info(`Running ${testScriptName} in ${targetAppPath}`); const result = await execa('npx', ['tsx', testScriptName], { cwd: targetAppPath, + stdio: 'inherit', reject: false, }); - const stdout = result.stdout || ''; - const stderr = result.stderr || ''; - - if (stdout) { - logger.debug(`[gen2-test-script] stdout:\n${stdout}`); - } - if (stderr) { - logger.debug(`[gen2-test-script] stderr:\n${stderr}`); - } - - const testResultLines = stdout.split('\n').filter((line) => { - const trimmed = line.trim(); - return ( - trimmed.startsWith('✅') || - trimmed.startsWith('❌') || - trimmed.includes('TEST SUMMARY') || - trimmed.includes('All tests passed') || - trimmed.includes('test(s) failed') - ); - }); - - for (const line of testResultLines) { - logger.info(`[gen2-test-script] ${line.trim()}`); - } - if (result.exitCode !== 0) { - const combinedOutput = [stdout, stderr].filter(Boolean).join('\n'); - throw new Error(`${testScriptName} failed with exit code ${result.exitCode}\n${combinedOutput}`); + throw new Error(`${testScriptName} failed with exit code ${result.exitCode}\n`); } logger.info(`${testScriptName} completed successfully`); @@ -506,7 +456,7 @@ async function amplifyPush(targetAppPath: string): Promise { try { const result = await execa(amplifyPath, ['push', '--yes', '--debug'], { cwd: targetAppPath, - // stdio: 'inherit', + stdio: logger.isDebug() ? 'inherit' : 'pipe', }); if (result.exitCode !== 0) { @@ -525,13 +475,11 @@ async function amplifyPush(targetAppPath: string): Promise { */ async function initializeAppFromCLI(params: InitializeAppFromCLIParams): Promise { const { appName, deploymentName, config, migrationTargetPath, envName, profile } = params; - const context = { appName, operation: 'initializeApp' }; - - logger.info(`Starting initialization for ${appName} with deployment name: ${deploymentName}`, context); + logger.info(`Starting initialization for ${appName} with deployment name: ${deploymentName}`); const sourceAppPath = appSelector.getAppPath(appName); - logger.debug(`Source app path: ${sourceAppPath}`, context); - logger.debug(`Config app name: ${config.app.name}`, context); + logger.debug(`Source app path: ${sourceAppPath}`); + logger.debug(`Config app name: ${config.app.name}`); // Derive sourceAppsBasePath once for all test script calls const sourceAppsBasePath = path.dirname(sourceAppPath); @@ -543,7 +491,7 @@ async function initializeAppFromCLI(params: InitializeAppFromCLIParams): Promise appName: deploymentName, }); - logger.debug(`Copying source directory to target...`, context); + logger.info(`Copying source directory to target...`); await directoryManager.copyDirectory(sourceAppPath, targetAppPath); // Update package.json name to use deploymentName for predictable Gen2 stack naming @@ -551,12 +499,12 @@ async function initializeAppFromCLI(params: InitializeAppFromCLIParams): Promise const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf-8')) as { name: string }; packageJson.name = deploymentName; await fs.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8'); - logger.debug(`Updated package.json name to ${deploymentName}`, context); + logger.debug(`Updated package.json name to ${deploymentName}`); - logger.debug(`Running amplify init in ${targetAppPath}`, context); + logger.info(`Running amplify init in ${targetAppPath}`); // Amplify init - logger.debug(`Using AWS profile '${profile}' for Amplify initialization`, context); + logger.debug(`Using AWS profile '${profile}' for Amplify initialization`); await amplifyInitializer.initializeApp({ appPath: targetAppPath, config, @@ -566,7 +514,7 @@ async function initializeAppFromCLI(params: InitializeAppFromCLIParams): Promise }); // Initialize categories (auth, api, storage, function, etc.) - logger.info(`Initializing categories for ${deploymentName}...`, context); + logger.info(`Initializing categories for ${deploymentName}...`); const categoryResult = await categoryInitializer.initializeCategories({ appPath: targetAppPath, config, @@ -574,14 +522,14 @@ async function initializeAppFromCLI(params: InitializeAppFromCLIParams): Promise }); if (categoryResult.initializedCategories.length > 0) { - logger.info(`Successfully initialized categories: ${categoryResult.initializedCategories.join(', ')}`, context); + logger.info(`Successfully initialized categories: ${categoryResult.initializedCategories.join(', ')}`); } if (categoryResult.skippedCategories.length > 0) { - logger.warn(`Skipped categories: ${categoryResult.skippedCategories.join(', ')}`, context); + logger.warn(`Skipped categories: ${categoryResult.skippedCategories.join(', ')}`); } if (categoryResult.errors.length > 0) { for (const error of categoryResult.errors) { - logger.error(`Category '${error.category}' failed: ${error.error}`, undefined, context); + logger.error(`Category '${error.category}' failed: ${error.error}`, undefined); } throw new Error(`Failed to initialize ${categoryResult.errors.length} category(ies)`); } @@ -589,95 +537,101 @@ async function initializeAppFromCLI(params: InitializeAppFromCLIParams): Promise // Run configure.sh if present (copies custom source files into amplify/backend/) const configureScriptPath = path.join(targetAppPath, 'configure.sh'); if (fs.existsSync(configureScriptPath)) { - logger.info(`Running configure.sh for ${deploymentName}...`, context); - await execa('bash', ['configure.sh'], { cwd: targetAppPath }); - logger.info(`Successfully ran configure.sh for ${deploymentName}`, context); + logger.info(`Running configure.sh for ${deploymentName}...`); + await execa('bash', ['configure.sh'], { cwd: targetAppPath, stdio: 'inherit' }); + logger.info(`Successfully ran configure.sh for ${deploymentName}`); } else { - logger.debug(`No configure.sh found for ${deploymentName}, skipping`, context); + logger.debug(`No configure.sh found for ${deploymentName}, skipping`); } // Push the initialized app to AWS - logger.info(`Pushing ${deploymentName} to AWS...`, context); + logger.info(`Pushing ${deploymentName} to AWS...`); await amplifyPush(targetAppPath); - logger.info(`Successfully pushed ${deploymentName} to AWS`, context); + logger.info(`Successfully pushed ${deploymentName} to AWS`); // Run gen1 test script to validate the Gen1 deployment - logger.info(`Running gen1 test script (post-push) for ${deploymentName}...`, context); + logger.info(`Running gen1 test script (post-push) for ${deploymentName}...`); await runGen1TestScript(targetAppPath, migrationTargetPath, sourceAppsBasePath); - logger.info(`Gen1 test script passed (post-push) for ${deploymentName}`, context); + logger.info(`Gen1 test script passed (post-push) for ${deploymentName}`); // Initialize git repo and commit the Gen1 state - logger.info(`Initializing git repository for ${deploymentName}...`, context); + logger.info(`Initializing git repository for ${deploymentName}...`); await execa('git', ['init'], { cwd: targetAppPath }); await execa('git', ['add', '.'], { cwd: targetAppPath }); await execa('git', ['commit', '-m', 'feat: gen1 initial commit'], { cwd: targetAppPath }); - logger.info(`Git repository initialized and Gen1 state committed`, context); + logger.info(`Git repository initialized and Gen1 state committed`); // Run gen2-migration pre-deployment workflow (lock -> checkout -> generate) - logger.info(`Running gen2-migration pre-deployment workflow for ${deploymentName}...`, context); + logger.info(`Running gen2-migration pre-deployment workflow for ${deploymentName}...`); await gen2MigrationExecutor.runPreDeploymentWorkflow(targetAppPath, envName); - logger.info(`Successfully completed gen2-migration pre-deployment workflow for ${deploymentName}`, context); + logger.info(`Successfully completed gen2-migration pre-deployment workflow for ${deploymentName}`); // Run app-specific post-generate script await runPostGenerateScript(appName, targetAppPath, envName); // Commit Gen2 generated code - logger.info(`Committing Gen2 generated code for ${deploymentName}...`, context); + logger.info(`Committing Gen2 generated code for ${deploymentName}...`); await execa('git', ['add', '.'], { cwd: targetAppPath }); await execa('git', ['commit', '-m', 'feat: gen2 migration generate'], { cwd: targetAppPath }); - logger.info(`Gen2 generated code committed`, context); + logger.info(`Gen2 generated code committed`); // Deploy Gen2 using ampx sandbox - logger.info(`Deploying Gen2 app using ampx sandbox for ${deploymentName}...`, context); + logger.info(`Deploying Gen2 app using ampx sandbox for ${deploymentName}...`); const gen2BranchName = `gen2-${envName}`; const gen2StackName = await gen2MigrationExecutor.deployGen2Sandbox(targetAppPath, deploymentName, gen2BranchName); - logger.info(`Gen2 app deployed with stack name: ${gen2StackName}`, context); + logger.info(`Gen2 app deployed with stack name: ${gen2StackName}`); // Run gen2 test script to validate the Gen2 code before deploying - logger.info(`Running gen2 test script (post-generate) for ${deploymentName}...`, context); + logger.info(`Running gen2 test script (post-generate) for ${deploymentName}...`); await runGen2TestScript(targetAppPath, migrationTargetPath, sourceAppsBasePath); - logger.info(`Gen2 test script passed (post-generate) for ${deploymentName}`, context); + logger.info(`Gen2 test script passed (post-generate) for ${deploymentName}`); // Checkout back to main branch for refactor (refactor must run from Gen1 branch) - logger.info(`Checking out main branch for refactor (refactor requires Gen1 files)...`, context); + logger.info(`Checking out main branch for refactor (refactor requires Gen1 files)...`); + await execa('git', ['add', '.'], { cwd: targetAppPath }); + await execa('git', ['commit', '--allow-empty', '-m', 'chore: before refactor'], { cwd: targetAppPath }); await execa('git', ['checkout', 'main'], { cwd: targetAppPath }); + logger.info('Installing dependencies...'); + await execa('npm', ['install'], { cwd: targetAppPath }); // Run refactor to move stateful resources from Gen1 to Gen2 - logger.info(`Running gen2-migration refactor for ${deploymentName}...`, context); + logger.info(`Running gen2-migration refactor for ${deploymentName}...`); await gen2MigrationExecutor.refactor(targetAppPath, gen2StackName); - logger.info(`Successfully completed gen2-migration refactor for ${deploymentName}`, context); + logger.info(`Successfully completed gen2-migration refactor for ${deploymentName}`); // Run gen1 test script to validate post-refactor state - logger.info(`Running gen1 test script (post-refactor) for ${deploymentName}...`, context); + logger.info(`Running gen1 test script (post-refactor) for ${deploymentName}...`); await runGen1TestScript(targetAppPath, migrationTargetPath, sourceAppsBasePath); - logger.info(`Gen1 test script passed (post-refactor) for ${deploymentName}`, context); + logger.info(`Gen1 test script passed (post-refactor) for ${deploymentName}`); // Checkout back to Gen2 branch for post-refactor edits - logger.info(`Checking out ${gen2BranchName} branch for post-refactor edits...`, context); + logger.info(`Checking out ${gen2BranchName} branch for post-refactor edits...`); + await execa('git', ['add', '.'], { cwd: targetAppPath }); + await execa('git', ['commit', '--allow-empty', '-m', 'chore: after refactor'], { cwd: targetAppPath }); await execa('git', ['checkout', gen2BranchName], { cwd: targetAppPath }); // Run app-specific post-refactor script await runPostRefactorScript(appName, targetAppPath, envName); // Commit post-refactor changes - logger.info(`Committing post-refactor changes for ${deploymentName}...`, context); + logger.info(`Committing post-refactor changes for ${deploymentName}...`); await execa('git', ['add', '.'], { cwd: targetAppPath }); await execa('git', ['commit', '-m', 'fix: post-refactor edits'], { cwd: targetAppPath }); - logger.info(`Post-refactor changes committed`, context); + logger.info(`Post-refactor changes committed`); // Redeploy Gen2 to pick up post-refactor changes - logger.info(`Redeploying Gen2 app after refactor for ${deploymentName}...`, context); + logger.info(`Redeploying Gen2 app after refactor for ${deploymentName}...`); await gen2MigrationExecutor.deployGen2Sandbox(targetAppPath, deploymentName, gen2BranchName); - logger.info(`Gen2 app redeployed successfully`, context); + logger.info(`Gen2 app redeployed successfully`); // Run gen1 test script to validate final deployment - logger.info(`Running gen1 test script (post-redeployment) for ${deploymentName}...`, context); + logger.info(`Running gen1 test script (post-redeployment) for ${deploymentName}...`); await runGen1TestScript(targetAppPath, migrationTargetPath, sourceAppsBasePath); - logger.info(`Gen1 test script passed (post-redeployment) for ${deploymentName}`, context); + logger.info(`Gen1 test script passed (post-redeployment) for ${deploymentName}`); - logger.info(`App ${deploymentName} fully initialized and migrated at ${targetAppPath}`, context); + logger.info(`App ${deploymentName} fully initialized and migrated at ${targetAppPath}`); } catch (error) { - logger.error(`Failed to initialize ${appName}`, error as Error, context); + logger.error(`Failed to initialize ${appName}`, error as Error); throw error; } } diff --git a/packages/amplify-gen2-migration-e2e-system/src/core/amplify-initializer.ts b/packages/amplify-gen2-migration-e2e-system/src/core/amplify-initializer.ts index 47ac5768f73..69fa3771435 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/core/amplify-initializer.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/core/amplify-initializer.ts @@ -3,9 +3,18 @@ * Uses the e2e-core utilities for reliable amplify init execution */ -import { ILogger, IAppInitializer, InitializeAppOptions } from '../interfaces'; -import { AppConfiguration, LogContext } from '../types'; +import { AppConfiguration } from '../types'; import { initJSProjectWithProfile } from '@aws-amplify/amplify-e2e-core'; +import { Logger } from '../utils/logger'; + +export interface InitializeAppOptions { + appPath: string; + config: AppConfiguration; + deploymentName: string; + /** Amplify environment name (required, 2-10 lowercase letters) */ + envName: string; + profile: string; +} export interface AmplifyInitSettings { name: string; @@ -31,18 +40,16 @@ interface BuildInitSettingsOptions { profile: string; } -export class AmplifyInitializer implements IAppInitializer { - constructor(private readonly logger: ILogger) {} +export class AmplifyInitializer { + constructor(private readonly logger: Logger) {} async initializeApp(options: InitializeAppOptions): Promise { const { appPath, config, deploymentName, envName, profile } = options; - const context: LogContext = { appName: deploymentName, operation: 'initializeApp' }; - - this.logger.info(`Starting amplify init for ${deploymentName} (config: ${config.app.name})`, context); - this.logger.debug(`App path: ${appPath}`, context); - this.logger.debug(`Configuration: ${JSON.stringify(config, null, 2)}`, context); - this.logger.debug(`Deployment name: ${deploymentName}`, context); + this.logger.info(`Starting amplify init for ${deploymentName} (config: ${config.app.name})`); + this.logger.debug(`App path: ${appPath}`); + this.logger.debug(`Configuration: ${JSON.stringify(config, null, 2)}`); + this.logger.debug(`Deployment name: ${deploymentName}`); const appNameValidation = this.validateAppName(deploymentName); if (!appNameValidation.valid) { @@ -56,16 +63,16 @@ export class AmplifyInitializer implements IAppInitializer { const startTime = Date.now(); try { - this.logger.debug(`Calling initJSProjectWithProfile...`, context); + this.logger.debug(`Calling initJSProjectWithProfile...`); const settings = this.buildInitSettings({ config, deploymentName, profile, envName }); - this.logger.debug(`Init settings: ${JSON.stringify(settings, null, 2)}`, context); + this.logger.debug(`Init settings: ${JSON.stringify(settings, null, 2)}`); await initJSProjectWithProfile(appPath, settings); const duration = Date.now() - startTime; - this.logger.info(`Successfully initialized Amplify app in ${appPath}, ${deploymentName} (took ${duration}ms)`, context); + this.logger.info(`Successfully initialized Amplify app in ${appPath} (${duration}ms)`); } catch (error) { const duration = Date.now() - startTime; - this.logger.error(`Failed to initialize Amplify app: ${deploymentName} (failed after ${duration}ms)`, error as Error, context); + this.logger.error(`Failed to initialize Amplify app: ${deploymentName} (failed after ${duration}ms)`, error as Error); throw error; } } @@ -132,11 +139,10 @@ export class AmplifyInitializer implements IAppInitializer { }; // Log the settings being used - const context: LogContext = { appName: deploymentName, operation: 'buildInitSettings' }; - this.logger.debug(`Built init settings for ${deploymentName} (config: ${config.app.name}):`, context); - this.logger.debug(`- Name: ${settings.name}`, context); - this.logger.info(`Using Amplify environment name: ${settings.envName}`, context); - this.logger.debug(`- Using default selections for editor, framework, etc.`, context); + this.logger.debug(`Built init settings for ${deploymentName} (config: ${config.app.name}):`); + this.logger.debug(`- Name: ${settings.name}`); + this.logger.info(`Using Amplify environment name: ${settings.envName}`); + this.logger.debug(`- Using default selections for editor, framework, etc.`); return settings; } diff --git a/packages/amplify-gen2-migration-e2e-system/src/core/app-selector.ts b/packages/amplify-gen2-migration-e2e-system/src/core/app-selector.ts index 1709f990db2..8543156cd10 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/core/app-selector.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/core/app-selector.ts @@ -3,14 +3,15 @@ */ import * as path from 'path'; -import { IAppSelector, ILogger, IFileManager } from '../interfaces'; import { CLIOptions } from '../types'; +import { Logger } from '../utils/logger'; +import { FileManager } from '../utils/file-manager'; -export class AppSelector implements IAppSelector { +export class AppSelector { private readonly appsBasePath: string; private availableApps?: string[]; - constructor(private readonly logger: ILogger, private readonly fileManager: IFileManager, appsBasePath = '../../amplify-migration-apps') { + constructor(private readonly logger: Logger, private readonly fileManager: FileManager, appsBasePath = '../../amplify-migration-apps') { // Resolve path relative to the project root, not the current file this.appsBasePath = path.resolve(process.cwd(), appsBasePath); } @@ -30,7 +31,7 @@ export class AppSelector implements IAppSelector { const appDirectories = await this.fileManager.listDirectories(this.appsBasePath); this.availableApps = appDirectories.sort(); - this.logger.info(`Discovered ${this.availableApps.length} available apps: ${this.availableApps.join(', ')}`); + this.logger.debug(`Discovered ${this.availableApps.length} available apps: ${this.availableApps.join(', ')}`); return this.availableApps; } catch (error) { @@ -38,7 +39,7 @@ export class AppSelector implements IAppSelector { } } - async validateAppExists(appName: string): Promise { + public async validateAppExists(appName: string): Promise { this.logger.debug(`Validating app exists: ${appName}`); const availableApps = await this.discoverAvailableApps(); @@ -52,7 +53,7 @@ export class AppSelector implements IAppSelector { } async selectApp(options: CLIOptions): Promise { - this.logger.debug('Selecting app based on CLI option', { operation: 'selectApp' }); + this.logger.debug('Selecting app based on CLI option'); const availableApps = await this.discoverAvailableApps(); diff --git a/packages/amplify-gen2-migration-e2e-system/src/core/category-initializer.ts b/packages/amplify-gen2-migration-e2e-system/src/core/category-initializer.ts index 39eecf85fa3..789689f8e67 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/core/category-initializer.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/core/category-initializer.ts @@ -3,10 +3,8 @@ * Uses the e2e-core utilities for reliable category initialization */ -import { ILogger } from '../interfaces'; import { AppConfiguration, - LogContext, APIConfiguration, AuthConfiguration, StorageConfiguration, @@ -34,6 +32,7 @@ import { } from '@aws-amplify/amplify-e2e-core'; import * as fs from 'fs'; import * as path from 'path'; +import { Logger } from '../utils/logger'; export interface CategoryInitializerOptions { appPath: string; @@ -48,14 +47,13 @@ export interface InitializeCategoriesResult { } export class CategoryInitializer { - constructor(private readonly logger: ILogger) {} + constructor(private readonly logger: Logger) {} /** * Initialize all categories defined in the configuration */ async initializeCategories(options: CategoryInitializerOptions): Promise { const { appPath, config, deploymentName } = options; - const context: LogContext = { appName: deploymentName, operation: 'initializeCategories' }; const result: InitializeCategoriesResult = { initializedCategories: [], @@ -63,11 +61,11 @@ export class CategoryInitializer { errors: [], }; - this.logger.info(`Starting category initialization for ${deploymentName}`, context); + this.logger.info(`Starting category initialization for ${deploymentName}`); const categories = config.categories; if (!categories) { - this.logger.info('No categories defined in configuration', context); + this.logger.info('No categories defined in configuration'); return result; } @@ -80,37 +78,37 @@ export class CategoryInitializer { // 6. Trigger functions (need AppSync/DynamoDB tables to exist) // 7. REST API last (needs functions to exist) if (categories.auth) { - await this.initializeAuthCategory(appPath, categories.auth, result, context); + await this.initializeAuthCategory(appPath, categories.auth, result); } if (categories.analytics) { - await this.initializeAnalyticsCategory(appPath, categories.analytics, result, context); + await this.initializeAnalyticsCategory(appPath, categories.analytics, result); } // Initialize regular (non-trigger) functions before API if (categories.function) { - await this.initializeRegularFunctions(appPath, categories.function, result, context); + await this.initializeRegularFunctions(appPath, categories.function, result); } if (categories.storage) { - await this.initializeStorageCategory(appPath, categories.storage, categories.auth, result, context); + await this.initializeStorageCategory(appPath, categories.storage, categories.auth, result); } if (categories.api) { - await this.initializeApiCategory(appPath, categories.api, categories.function, result, context); + await this.initializeApiCategory(appPath, categories.api, categories.function, result); } // Initialize trigger functions after API (they need AppSync tables to exist) if (categories.function) { - await this.initializeTriggerFunctions(appPath, categories.function, result, context); + await this.initializeTriggerFunctions(appPath, categories.function, result); } // Initialize REST API separately if configured if (categories.restApi) { - await this.initializeRestApiCategory(appPath, categories.restApi, categories.function, result, context); + await this.initializeRestApiCategory(appPath, categories.restApi, categories.function, result); } - this.logger.info(`Category initialization complete. Initialized: ${result.initializedCategories.join(', ') || 'none'}`, context); + this.logger.info(`Category initialization complete. Initialized: ${result.initializedCategories.join(', ') || 'none'}`); return result; } @@ -120,12 +118,7 @@ export class CategoryInitializer { * Supports: social providers, user pool groups * Not yet supported: auth triggers (preSignUp, etc.) */ - private async initializeAuthCategory( - appPath: string, - authConfig: AuthConfiguration, - result: InitializeCategoriesResult, - context: LogContext, - ): Promise { + private async initializeAuthCategory(appPath: string, authConfig: AuthConfiguration, result: InitializeCategoriesResult): Promise { const hasSocialProviders = authConfig.socialProviders && authConfig.socialProviders.length > 0; const hasUserPoolGroups = authConfig.userPoolGroups && authConfig.userPoolGroups.length > 0; const hasAuthTriggers = authConfig.triggers && Object.keys(authConfig.triggers).length > 0; @@ -137,27 +130,27 @@ export class CategoryInitializer { if (hasAuthTriggers) features.push('triggers (not yet supported)'); const authType = features.length > 0 ? `with ${features.join(', ')}` : 'with default settings'; - this.logger.info(`Initializing auth category ${authType}...`, context); + this.logger.info(`Initializing auth category ${authType}...`); // Warn about unsupported features if (hasAuthTriggers) { - this.logger.warn('Auth triggers (preSignUp, postConfirmation, etc.) are not yet supported by category-initializer', context); + this.logger.warn('Auth triggers (preSignUp, postConfirmation, etc.) are not yet supported by category-initializer'); } try { if (hasUserPoolGroups) { // Use auth with groups (creates Admins and Users groups by default) // Note: addAuthWithGroups creates hardcoded "Admins" and "Users" groups - this.logger.debug(`User pool groups configured: ${authConfig.userPoolGroups?.join(', ')}`, context); + this.logger.debug(`User pool groups configured: ${authConfig.userPoolGroups?.join(', ')}`); await addAuthWithGroups(appPath); } else if (hasSocialProviders) { // Use social auth when social providers are configured // This sets up Cognito with Facebook, Google, and Amazon OAuth - this.logger.debug(`Social providers configured: ${authConfig.socialProviders.join(', ')}`, context); + this.logger.debug(`Social providers configured: ${authConfig.socialProviders.join(', ')}`); await addAuthWithDefaultSocial(appPath); } else if (authConfig.signInMethods?.includes('email')) { // Use email sign-in when explicitly configured - this.logger.debug('Using email sign-in method', context); + this.logger.debug('Using email sign-in method'); await addAuthWithEmail(appPath); } else { // Use default auth configuration (username sign-in) @@ -165,10 +158,10 @@ export class CategoryInitializer { } result.initializedCategories.push('auth'); - this.logger.info('Auth category initialized successfully', context); + this.logger.info('Auth category initialized successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to initialize auth category: ${errorMessage}`, error as Error, context); + this.logger.error(`Failed to initialize auth category: ${errorMessage}`, error as Error); result.errors.push({ category: 'auth', error: errorMessage }); } } @@ -181,18 +174,17 @@ export class CategoryInitializer { apiConfig: APIConfiguration, functionConfig: FunctionConfiguration | undefined, result: InitializeCategoriesResult, - context: LogContext, ): Promise { // Only handle GraphQL here; REST is handled separately if (apiConfig.type !== 'GraphQL') { // If type is REST but no restApi config, use legacy behavior if (apiConfig.type === 'REST') { - await this.initializeRestApiFromLegacyConfig(appPath, functionConfig, result, context); + await this.initializeRestApiFromLegacyConfig(appPath, functionConfig, result); } return; } - this.logger.info('Initializing GraphQL API category...', context); + this.logger.info('Initializing GraphQL API category...'); try { // Build authTypesConfig in the order specified by migration-config.json so the @@ -231,18 +223,18 @@ export class CategoryInitializer { const apiName = this.getApiNameFromBackend(appPath); if (apiName) { updateSchema(appPath, apiName, schemaContent); - this.logger.debug(`Updated schema from ${apiConfig.schema}`, context); + this.logger.debug(`Updated schema from ${apiConfig.schema}`); } } else { - this.logger.warn(`Schema file not found: ${schemaPath}`, context); + this.logger.warn(`Schema file not found: ${schemaPath}`); } } result.initializedCategories.push('api'); - this.logger.info('GraphQL API category initialized successfully', context); + this.logger.info('GraphQL API category initialized successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to initialize GraphQL API category: ${errorMessage}`, error as Error, context); + this.logger.error(`Failed to initialize GraphQL API category: ${errorMessage}`, error as Error); result.errors.push({ category: 'api', error: errorMessage }); } } @@ -255,14 +247,13 @@ export class CategoryInitializer { restApiConfig: RestApiConfiguration, functionConfig: FunctionConfiguration | undefined, result: InitializeCategoriesResult, - context: LogContext, ): Promise { - this.logger.info(`Initializing REST API category (${restApiConfig.name})...`, context); + this.logger.info(`Initializing REST API category (${restApiConfig.name})...`); // REST API requires at least one Lambda function to exist const hasFunctions = functionConfig && functionConfig.functions.length > 0; if (!hasFunctions) { - this.logger.warn('REST API requires at least one Lambda function, skipping', context); + this.logger.warn('REST API requires at least one Lambda function, skipping'); result.skippedCategories.push('restApi'); return; } @@ -270,7 +261,7 @@ export class CategoryInitializer { // Check if the specified lambda source exists const lambdaExists = functionConfig.functions.some((f) => f.name === restApiConfig.lambdaSource); if (!lambdaExists) { - this.logger.warn(`REST API lambda source '${restApiConfig.lambdaSource}' not found in functions, skipping`, context); + this.logger.warn(`REST API lambda source '${restApiConfig.lambdaSource}' not found in functions, skipping`); result.skippedCategories.push('restApi'); return; } @@ -286,10 +277,10 @@ export class CategoryInitializer { }); result.initializedCategories.push('restApi'); - this.logger.info('REST API category initialized successfully', context); + this.logger.info('REST API category initialized successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to initialize REST API category: ${errorMessage}`, error as Error, context); + this.logger.error(`Failed to initialize REST API category: ${errorMessage}`, error as Error); result.errors.push({ category: 'restApi', error: errorMessage }); } } @@ -301,13 +292,12 @@ export class CategoryInitializer { appPath: string, functionConfig: FunctionConfiguration | undefined, result: InitializeCategoriesResult, - context: LogContext, ): Promise { - this.logger.info('Initializing REST API category (legacy config)...', context); + this.logger.info('Initializing REST API category (legacy config)...'); const hasFunctions = functionConfig && functionConfig.functions.length > 0; if (!hasFunctions) { - this.logger.warn('REST API requires at least one Lambda function, skipping', context); + this.logger.warn('REST API requires at least one Lambda function, skipping'); result.skippedCategories.push('api'); return; } @@ -322,10 +312,10 @@ export class CategoryInitializer { }); result.initializedCategories.push('api'); - this.logger.info('REST API category initialized successfully', context); + this.logger.info('REST API category initialized successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to initialize REST API category: ${errorMessage}`, error as Error, context); + this.logger.error(`Failed to initialize REST API category: ${errorMessage}`, error as Error); result.errors.push({ category: 'api', error: errorMessage }); } } @@ -339,17 +329,16 @@ export class CategoryInitializer { storageConfig: StorageConfiguration, authConfig: AuthConfiguration | undefined, result: InitializeCategoriesResult, - context: LogContext, ): Promise { // Check if this is DynamoDB storage if (storageConfig.type === 'dynamodb' && storageConfig.tables && storageConfig.tables.length > 0) { - await this.initializeDynamoDBStorage(appPath, storageConfig, result, context); + await this.initializeDynamoDBStorage(appPath, storageConfig, result); return; } // S3 storage if (!storageConfig.buckets || storageConfig.buckets.length === 0) { - this.logger.warn('No storage buckets configured, skipping storage category', context); + this.logger.warn('No storage buckets configured, skipping storage category'); result.skippedCategories.push('storage'); return; } @@ -366,19 +355,19 @@ export class CategoryInitializer { const accessType = hasGuestAccess ? 'auth and guest' : 'auth-only'; const triggerInfo = hasTriggers ? ' with Lambda trigger' : ''; const groupInfo = hasUserPoolGroups ? ' (with user pool groups)' : ''; - this.logger.info(`Initializing S3 storage category with ${accessType} access${triggerInfo}${groupInfo}...`, context); + this.logger.info(`Initializing S3 storage category with ${accessType} access${triggerInfo}${groupInfo}...`); try { if (hasTriggers) { // Add S3 storage with Lambda trigger (creates a new trigger function) const projectHasFunctions = result.initializedCategories.includes('function'); - this.logger.debug(`Adding S3 storage with Lambda trigger (projectHasFunctions: ${projectHasFunctions})`, context); + this.logger.debug(`Adding S3 storage with Lambda trigger (projectHasFunctions: ${projectHasFunctions})`); await addS3WithTrigger(appPath, { projectHasFunctions }); } else if (hasUserPoolGroups) { // Use group-aware helper when user pool groups are configured. // addAuthWithGroups creates hardcoded "Admins" and "Users" groups regardless // of what the config specifies, so we must pass those names here. - this.logger.debug(`Adding S3 storage with group access (Admins, Users)`, context); + this.logger.debug(`Adding S3 storage with group access (Admins, Users)`); await addS3WithGroupAccess(appPath); } else if (hasGuestAccess) { // Add S3 storage with auth and guest access @@ -389,10 +378,10 @@ export class CategoryInitializer { } result.initializedCategories.push('storage'); - this.logger.info('Storage category initialized successfully', context); + this.logger.info('Storage category initialized successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to initialize storage category: ${errorMessage}`, error as Error, context); + this.logger.error(`Failed to initialize storage category: ${errorMessage}`, error as Error); result.errors.push({ category: 'storage', error: errorMessage }); } } @@ -404,20 +393,19 @@ export class CategoryInitializer { appPath: string, storageConfig: StorageConfiguration, result: InitializeCategoriesResult, - context: LogContext, ): Promise { const tables = storageConfig.tables; if (!tables || tables.length === 0) { - this.logger.warn('No DynamoDB tables configured, skipping storage category', context); + this.logger.warn('No DynamoDB tables configured, skipping storage category'); result.skippedCategories.push('storage'); return; } - this.logger.info(`Initializing DynamoDB storage with ${tables.length} table(s)...`, context); + this.logger.info(`Initializing DynamoDB storage with ${tables.length} table(s)...`); try { for (const table of tables) { - this.logger.debug(`Adding DynamoDB table: ${table.name}`, context); + this.logger.debug(`Adding DynamoDB table: ${table.name}`); // Use addDynamoDBWithGSIWithSettings if GSI is configured if (table.gsi && table.gsi.length > 0) { @@ -429,7 +417,7 @@ export class CategoryInitializer { } else { // For tables without GSI, we still use the GSI function but it will create default columns // This is a limitation of the e2e-core helpers - this.logger.warn(`DynamoDB table '${table.name}' without GSI - using default schema`, context); + this.logger.warn(`DynamoDB table '${table.name}' without GSI - using default schema`); await addDynamoDBWithGSIWithSettings(appPath, { resourceName: table.name, tableName: table.name, @@ -437,14 +425,14 @@ export class CategoryInitializer { }); } - this.logger.debug(`DynamoDB table ${table.name} added successfully`, context); + this.logger.debug(`DynamoDB table ${table.name} added successfully`); } result.initializedCategories.push('storage'); - this.logger.info('DynamoDB storage category initialized successfully', context); + this.logger.info('DynamoDB storage category initialized successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to initialize DynamoDB storage: ${errorMessage}`, error as Error, context); + this.logger.error(`Failed to initialize DynamoDB storage: ${errorMessage}`, error as Error); result.errors.push({ category: 'storage', error: errorMessage }); } } @@ -456,20 +444,19 @@ export class CategoryInitializer { appPath: string, functionConfig: FunctionConfiguration, result: InitializeCategoriesResult, - context: LogContext, ): Promise { const regularFunctions = functionConfig.functions.filter((f) => !f.trigger); if (regularFunctions.length === 0) { - this.logger.debug('No regular functions to initialize', context); + this.logger.debug('No regular functions to initialize'); return; } - this.logger.info(`Initializing ${regularFunctions.length} regular function(s)...`, context); + this.logger.info(`Initializing ${regularFunctions.length} regular function(s)...`); try { for (const func of regularFunctions) { - this.logger.debug(`Adding function: ${func.name}`, context); + this.logger.debug(`Adding function: ${func.name}`); const runtime = this.mapRuntime(func.runtime); const template = this.mapTemplate(func.template); @@ -483,14 +470,14 @@ export class CategoryInitializer { runtime, ); - this.logger.debug(`Function ${func.name} added successfully`, context); + this.logger.debug(`Function ${func.name} added successfully`); } result.initializedCategories.push('function'); - this.logger.info('Regular functions initialized successfully', context); + this.logger.info('Regular functions initialized successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to initialize regular functions: ${errorMessage}`, error as Error, context); + this.logger.error(`Failed to initialize regular functions: ${errorMessage}`, error as Error); result.errors.push({ category: 'function', error: errorMessage }); } } @@ -503,20 +490,19 @@ export class CategoryInitializer { appPath: string, functionConfig: FunctionConfiguration, result: InitializeCategoriesResult, - context: LogContext, ): Promise { const triggerFunctions = functionConfig.functions.filter((f) => f.trigger); if (triggerFunctions.length === 0) { - this.logger.debug('No trigger functions to initialize', context); + this.logger.debug('No trigger functions to initialize'); return; } - this.logger.info(`Initializing ${triggerFunctions.length} trigger function(s)...`, context); + this.logger.info(`Initializing ${triggerFunctions.length} trigger function(s)...`); try { for (const func of triggerFunctions) { - this.logger.debug(`Adding trigger function: ${func.name}`, context); + this.logger.debug(`Adding trigger function: ${func.name}`); const runtime = this.mapRuntime(func.runtime); const triggerType = func.trigger?.type; @@ -548,17 +534,17 @@ export class CategoryInitializer { addLambdaTrigger, ); } else { - this.logger.warn(`Unsupported trigger type '${triggerType}' for function ${func.name}, skipping`, context); + this.logger.warn(`Unsupported trigger type '${triggerType}' for function ${func.name}, skipping`); continue; } - this.logger.debug(`Trigger function ${func.name} added successfully`, context); + this.logger.debug(`Trigger function ${func.name} added successfully`); } - this.logger.info('Trigger functions initialized successfully', context); + this.logger.info('Trigger functions initialized successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to initialize trigger functions: ${errorMessage}`, error as Error, context); + this.logger.error(`Failed to initialize trigger functions: ${errorMessage}`, error as Error); result.errors.push({ category: 'function-triggers', error: errorMessage }); } } @@ -630,12 +616,11 @@ export class CategoryInitializer { appPath: string, analyticsConfig: AnalyticsConfiguration, result: InitializeCategoriesResult, - context: LogContext, ): Promise { - this.logger.info(`Initializing analytics category (${analyticsConfig.type}: ${analyticsConfig.name})...`, context); + this.logger.info(`Initializing analytics category (${analyticsConfig.type}: ${analyticsConfig.name})...`); if (analyticsConfig.type !== 'kinesis') { - this.logger.warn(`Analytics type '${analyticsConfig.type}' is not yet supported, skipping`, context); + this.logger.warn(`Analytics type '${analyticsConfig.type}' is not yet supported, skipping`); result.skippedCategories.push('analytics'); return; } @@ -649,10 +634,10 @@ export class CategoryInitializer { }); result.initializedCategories.push('analytics'); - this.logger.info('Analytics category initialized successfully', context); + this.logger.info('Analytics category initialized successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to initialize analytics category: ${errorMessage}`, error as Error, context); + this.logger.error(`Failed to initialize analytics category: ${errorMessage}`, error as Error); result.errors.push({ category: 'analytics', error: errorMessage }); } } diff --git a/packages/amplify-gen2-migration-e2e-system/src/core/cdk-atmosphere-integration.ts b/packages/amplify-gen2-migration-e2e-system/src/core/cdk-atmosphere-integration.ts index 69184091919..1b97219bb4d 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/core/cdk-atmosphere-integration.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/core/cdk-atmosphere-integration.ts @@ -3,11 +3,12 @@ * Handles detection and integration with Atmosphere environments while supporting local AWS configurations */ -import { ICDKAtmosphereIntegration, ILogger, IEnvironmentDetector, IAWSProfileManager } from '../interfaces'; -import { EnvironmentType, LogContext, AtmosphereAllocation } from '../types'; +import { EnvironmentType, AtmosphereAllocation } from '../types'; import { AtmosphereClient } from '@cdklabs/cdk-atmosphere-client'; import { AWSProfileManager } from '../utils/aws-profile-manager'; import * as crypto from 'crypto'; +import { Logger } from '../utils/logger'; +import { EnvironmentDetector } from './environment-detector'; // Amplify supported regions (copied from @aws-amplify/amplify-e2e-core to avoid ESM import issues) const amplifyRegions = [ @@ -35,15 +36,15 @@ const amplifyRegions = [ // Maximum number of allocation attempts to get a supported Amplify region const MAX_ALLOCATION_ATTEMPTS = 10; -export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { +export class CDKAtmosphereIntegration { private atmosphereClient?: AtmosphereClient; private cachedAllocation?: AtmosphereAllocation; private allocationId?: string; private currentProfileName?: string; - private readonly profileManager: IAWSProfileManager; + private readonly profileManager: AWSProfileManager; private readonly supportedRegions: Set; - constructor(private readonly logger: ILogger, private readonly environmentDetector: IEnvironmentDetector, homeDir?: string) { + constructor(private readonly logger: Logger, private readonly environmentDetector: EnvironmentDetector, homeDir?: string) { this.profileManager = new AWSProfileManager(logger, homeDir); this.supportedRegions = new Set(amplifyRegions); } @@ -66,26 +67,23 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { } async isAtmosphereEnvironment(): Promise { - const context: LogContext = { operation: 'isAtmosphereEnvironment' }; - try { const envType = await this.environmentDetector.detectEnvironment(); const isAtmosphere = envType === EnvironmentType.ATMOSPHERE; - this.logger.debug(`Environment type detected: ${envType}`, context); + this.logger.debug(`Environment type detected: ${envType}`); return isAtmosphere; } catch (error) { - this.logger.error('Failed to detect environment type', error as Error, context); + this.logger.error('Failed to detect environment type', error as Error); return false; } } async initializeForAtmosphere(): Promise { - const context: LogContext = { operation: 'initializeForAtmosphere' }; - this.logger.info('Initializing CDK Atmosphere client for Atmosphere environment', context); + this.logger.info('Initializing CDK Atmosphere client for Atmosphere environment'); if (this.cachedAllocation) { - this.logger.debug('Using cached Atmosphere credentials', context); + this.logger.debug('Using cached Atmosphere credentials'); return this.cachedAllocation; } @@ -97,13 +95,13 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { logStream: process.stdout, }); - this.logger.debug(`Initialized Atmosphere client with endpoint: ${atmosphereEndpoint}`, context); + this.logger.debug(`Initialized Atmosphere client with endpoint: ${atmosphereEndpoint}`); const allocation = await this.getAtmosphereAllocation(); this.cachedAllocation = allocation; - this.logger.info('Successfully initialized CDK Atmosphere client', context); + this.logger.info('Successfully initialized CDK Atmosphere client'); return allocation; } @@ -112,8 +110,6 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { * Returns the generated profile name that can be used with AWS CLI/SDK. */ async getProfileFromAllocation(): Promise { - const context: LogContext = { operation: 'getProfileFromAllocation' }; - const isAtmosphere = await this.isAtmosphereEnvironment(); if (!isAtmosphere) { @@ -137,7 +133,7 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { region: allocation.region, }); - this.logger.info(`Created AWS profile: ${profileName}`, context); + this.logger.info(`Created AWS profile: ${profileName}`); return profileName; } catch (atmosphereError) { throw Error(`Atmosphere credentials failed: ${(atmosphereError as Error).message}`); @@ -145,7 +141,6 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { } async cleanup(): Promise { - const context: LogContext = { operation: 'cleanup' }; const isAtmosphere = await this.isAtmosphereEnvironment(); if (!isAtmosphere) { @@ -153,14 +148,14 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { return; } - this.logger.debug('Cleaning up CDK Atmosphere client', context); + this.logger.debug('Cleaning up CDK Atmosphere client'); try { if (this.currentProfileName) { - this.logger.debug(`Removing AWS profile: ${this.currentProfileName}`, context); + this.logger.debug(`Removing AWS profile: ${this.currentProfileName}`); try { await this.profileManager.removeProfile(this.currentProfileName); - this.logger.debug(`Successfully removed AWS profile: ${this.currentProfileName}`, context); + this.logger.debug(`Successfully removed AWS profile: ${this.currentProfileName}`); } catch (profileError) { this.logger.warn(`Failed to remove AWS profile ${this.currentProfileName}: ${(profileError as Error).message}`); } @@ -169,7 +164,7 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { // Release the Atmosphere allocation if we have one if (this.atmosphereClient && this.allocationId) { - this.logger.debug(`Releasing Atmosphere allocation: ${this.allocationId}`, context); + this.logger.debug(`Releasing Atmosphere allocation: ${this.allocationId}`); await this.atmosphereClient.release(this.allocationId, 'success'); this.allocationId = undefined; } @@ -178,16 +173,14 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { this.atmosphereClient = undefined; this.cachedAllocation = undefined; - this.logger.debug('Successfully cleaned up CDK Atmosphere client', context); + this.logger.debug('Successfully cleaned up CDK Atmosphere client'); } catch (error) { - this.logger.error('Failed to cleanup CDK Atmosphere client', error as Error, context); + this.logger.error('Failed to cleanup CDK Atmosphere client', error as Error); throw error; } } private async getAtmosphereAllocation(): Promise { - const context: LogContext = { operation: 'getAtmosphereCredentials' }; - if (!this.atmosphereClient) { throw Error('Atmosphere client not initialized'); } @@ -202,10 +195,7 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { // Retry loop to get an allocation in a supported Amplify region, Atmosphere doesn't support requesting a specific region for (let attempt = 1; attempt <= MAX_ALLOCATION_ATTEMPTS; attempt++) { try { - this.logger.debug( - `Attempt ${attempt}/${MAX_ALLOCATION_ATTEMPTS}: Acquiring environment allocation from pool: ${poolName}`, - context, - ); + this.logger.debug(`Attempt ${attempt}/${MAX_ALLOCATION_ATTEMPTS}: Acquiring environment allocation from pool: ${poolName}`); const allocation = await this.atmosphereClient.acquire({ pool: poolName, @@ -233,10 +223,7 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { // Check if the region is supported by Amplify if (!this.isRegionSupported(environment.region)) { - this.logger.warn( - `Allocation ${allocation.id} is in unsupported region ${environment.region}. Releasing and retrying...`, - context, - ); + this.logger.warn(`Allocation ${allocation.id} is in unsupported region ${environment.region}. Releasing and retrying...`); // Release this allocation and try again await this.atmosphereClient.release(allocation.id, 'success'); @@ -255,9 +242,8 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { this.logger.info( `Successfully acquired allocation ${allocation.id} in supported region ${environment.region} (attempt ${attempt})`, - context, ); - this.logger.debug(`Account: ${environment.account}, Region: ${environment.region}`, context); + this.logger.debug(`Account: ${environment.account}, Region: ${environment.region}`); return atmosphereAllocation; } catch (error) { @@ -266,13 +252,12 @@ export class CDKAtmosphereIntegration implements ICDKAtmosphereIntegration { this.logger.error( `Failed to get Atmosphere allocation in supported region after ${MAX_ALLOCATION_ATTEMPTS} attempts`, error as Error, - context, ); throw error; } // Log and continue to next attempt - this.logger.warn(`Attempt ${attempt} failed: ${(error as Error).message}. Retrying...`, context); + this.logger.warn(`Attempt ${attempt} failed: ${(error as Error).message}. Retrying...`); } } diff --git a/packages/amplify-gen2-migration-e2e-system/src/core/configuration-loader.ts b/packages/amplify-gen2-migration-e2e-system/src/core/configuration-loader.ts index 4c8a794fe0b..87e0ed79b85 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/core/configuration-loader.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/core/configuration-loader.ts @@ -4,20 +4,21 @@ import * as fs from 'fs-extra'; import * as path from 'path'; -import { IConfigurationLoader, ILogger, IFileManager } from '../interfaces'; import { AppConfiguration, ValidationResult } from '../types'; +import { Logger } from '../utils/logger'; +import { FileManager } from '../utils/file-manager'; -export class ConfigurationLoader implements IConfigurationLoader { +export class ConfigurationLoader implements ConfigurationLoader { private readonly appsBasePath: string; private readonly configFileName = 'migration-config.json'; - constructor(private readonly logger: ILogger, private readonly fileManager: IFileManager, appsBasePath = '../../amplify-migration-apps') { + constructor(private readonly logger: Logger, private readonly fileManager: FileManager, appsBasePath = '../../amplify-migration-apps') { // Resolve path relative to the project root, not the current file this.appsBasePath = path.resolve(process.cwd(), appsBasePath); } async loadAppConfiguration(appName: string): Promise { - this.logger.debug(`Loading configuration for app: ${appName}`, { appName }); + this.logger.debug(`Loading configuration for app: ${appName}`); const configPath = this.getConfigPath(appName); @@ -42,7 +43,7 @@ export class ConfigurationLoader implements IConfigurationLoader { throw Error('App configuration did not pass validation.'); } - this.logger.info(`Successfully loaded configuration for ${appName}`, { appName }); + this.logger.info(`Successfully loaded configuration for ${appName}`); return config; } catch (error) { throw new Error(`Failed to load configuration for ${appName}: ${(error as Error).message}`); diff --git a/packages/amplify-gen2-migration-e2e-system/src/core/environment-detector.ts b/packages/amplify-gen2-migration-e2e-system/src/core/environment-detector.ts index 77b8d15d644..682d5972fc2 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/core/environment-detector.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/core/environment-detector.ts @@ -2,14 +2,14 @@ * Environment detection for Atmosphere vs Local environments */ -import { IEnvironmentDetector, ILogger } from '../interfaces'; import { EnvironmentType } from '../types'; +import { Logger } from '../utils/logger'; -export class EnvironmentDetector implements IEnvironmentDetector { +export class EnvironmentDetector implements EnvironmentDetector { private detectedEnvironment?: EnvironmentType; private environmentVariables: Record; - constructor(private readonly logger: ILogger) { + constructor(private readonly logger: Logger) { this.environmentVariables = Object.fromEntries(Object.entries(process.env).filter(([, value]) => value !== undefined)) as Record< string, string @@ -56,15 +56,4 @@ export class EnvironmentDetector implements IEnvironmentDetector { this.environmentVariables.JENKINS_URL ); } - - getEnvironmentSummary(): Record { - return { - type: this.detectedEnvironment, - nodeVersion: process.version, - platform: process.platform, - architecture: process.arch, - workingDirectory: process.cwd(), - isCI: this.isCI(), - }; - } } diff --git a/packages/amplify-gen2-migration-e2e-system/src/core/gen2-migration-executor.ts b/packages/amplify-gen2-migration-e2e-system/src/core/gen2-migration-executor.ts index 55fdecd4961..d5e87a5f986 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/core/gen2-migration-executor.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/core/gen2-migration-executor.ts @@ -7,9 +7,8 @@ import execa from 'execa'; import os from 'os'; -import { ILogger } from '../interfaces'; -import { LogContext } from '../types'; import { getCLIPath } from '@aws-amplify/amplify-e2e-core'; +import { Logger } from '../utils/logger'; /** * Available gen2-migration steps @@ -36,7 +35,7 @@ export class Gen2MigrationExecutor { private readonly amplifyPath: string; private readonly profile?: string; - constructor(private readonly logger: ILogger, options?: Gen2MigrationExecutorOptions) { + constructor(private readonly logger: Logger, options?: Gen2MigrationExecutorOptions) { this.amplifyPath = getCLIPath(true); this.profile = options?.profile; } @@ -45,14 +44,12 @@ export class Gen2MigrationExecutor { * Execute a gen2-migration step. Throws on failure. */ private async executeStep(step: Gen2MigrationStep, appPath: string, extraArgs: string[] = []): Promise { - const context: LogContext = { operation: `gen2-migration-${step}` }; - - this.logger.info(`Executing gen2-migration ${step}...`, context); - this.logger.debug(`App path: ${appPath}`, context); - this.logger.debug(`Using amplify CLI at: ${this.amplifyPath}`, context); + this.logger.info(`Executing gen2-migration ${step}...`); + this.logger.debug(`App path: ${appPath}`); + this.logger.debug(`Using amplify CLI at: ${this.amplifyPath}`); const args = ['gen2-migration', step, '--yes', ...extraArgs]; - this.logger.debug(`Command: ${this.amplifyPath} ${args.join(' ')}`, context); + this.logger.debug(`Command: ${this.amplifyPath} ${args.join(' ')}`); const startTime = Date.now(); @@ -61,43 +58,20 @@ export class Gen2MigrationExecutor { const result = await execa(this.amplifyPath, args, { cwd: appPath, + stdio: 'inherit', reject: false, env, - all: true, // Combine stdout and stderr }); const durationMs = Date.now() - startTime; - const combinedOutput = result.all ?? `${result.stdout}\n${result.stderr}`; - - // Always log output for debugging - if (combinedOutput.trim()) { - const outputLines = combinedOutput.split('\n'); - const lastLines = outputLines.slice(-100).join('\n'); - this.logger.info(`gen2-migration ${step} output:\n${lastLines}`, context); - } - - // Check for command not found - CLI returns exit code 0 but prints help - const commandNotFound = this.checkForCommandNotFound(combinedOutput); - if (commandNotFound) { - this.logger.error(`gen2-migration ${step} command not recognized by CLI`, undefined, context); - throw new Error(`gen2-migration ${step} failed: command not found. Ensure you are using the correct amplify CLI version.`); - } if (result.exitCode !== 0) { const errorMessage = result.stderr || result.stdout || `Exit code ${result.exitCode}`; - this.logger.error(`gen2-migration ${step} failed: ${errorMessage}`, undefined, context); + this.logger.error(`gen2-migration ${step} failed: ${errorMessage}`, undefined); throw new Error(`gen2-migration ${step} failed: ${errorMessage}`); } - this.logger.info(`gen2-migration ${step} completed (${durationMs}ms)`, context); - } - - /** - * Check if the CLI output indicates the command was not found. - * The CLI returns exit code 0 but prints a warning when command is not recognized. - */ - private checkForCommandNotFound(output: string): boolean { - return output.includes('The Amplify CLI can NOT find command'); + this.logger.info(`gen2-migration ${step} completed (${durationMs}ms)`); } /** @@ -118,6 +92,8 @@ export class Gen2MigrationExecutor { */ public async generate(appPath: string): Promise { await this.executeStep('generate', appPath); + this.logger.info('Installing dependencies..'); + await execa('npm', ['install'], { cwd: appPath }); } /** @@ -133,21 +109,22 @@ export class Gen2MigrationExecutor { * Run pre-deployment workflow: lock -> checkout gen2 branch -> generate */ public async runPreDeploymentWorkflow(appPath: string, envName = 'main'): Promise { - const context: LogContext = { operation: 'gen2-migration-workflow' }; - this.logger.info('Starting pre-deployment workflow (lock -> checkout -> generate)...', context); + this.logger.info('Starting pre-deployment workflow (lock -> checkout -> generate)...'); // Lock on the main branch await this.lock(appPath); // Create and checkout gen2 branch before generate const gen2BranchName = `gen2-${envName}`; - this.logger.info(`Creating and checking out branch '${gen2BranchName}'...`, context); + this.logger.info(`Creating and checking out branch '${gen2BranchName}'...`); + await execa('git', ['add', '.'], { cwd: appPath }); + await execa('git', ['commit', '--allow-empty', '-m', 'chore: before generate'], { cwd: appPath }); await execa('git', ['checkout', '-b', gen2BranchName], { cwd: appPath }); // Generate Gen2 code await this.generate(appPath); - this.logger.info('Pre-deployment workflow completed', context); + this.logger.info('Pre-deployment workflow completed'); } /** @@ -161,11 +138,9 @@ export class Gen2MigrationExecutor { * @param branchName - Branch name to set as AWS_BRANCH env var for unique resource naming */ public async deployGen2Sandbox(appPath: string, deploymentName: string, branchName: string): Promise { - const context: LogContext = { operation: 'gen2-sandbox-deploy' }; - - this.logger.info('Deploying Gen2 app using ampx sandbox...', context); - this.logger.debug(`App path: ${appPath}`, context); - this.logger.debug(`Branch name (AWS_BRANCH): ${branchName}`, context); + this.logger.info('Deploying Gen2 app using ampx sandbox...'); + this.logger.debug(`App path: ${appPath}`); + this.logger.debug(`Branch name (AWS_BRANCH): ${branchName}`); const startTime = Date.now(); @@ -177,28 +152,22 @@ export class Gen2MigrationExecutor { ...(this.profile && { AWS_PROFILE: this.profile }), }; + this.logger.info('Installing dependencies...'); + await execa('npm', ['install'], { cwd: appPath }); const result = await execa('npx', ['ampx', 'sandbox', '--once'], { cwd: appPath, reject: false, + stdio: 'inherit', env, - all: true, // Combine stdout and stderr into result.all }); const durationMs = Date.now() - startTime; - // Use result.all if available (combined output), otherwise combine manually - const combinedOutput = result.all ?? `${result.stdout}\n${result.stderr}`; - - // Check for errors in output (ampx sandbox may return exit code 0 even on failure) - const hasError = this.checkForAmpxErrors(combinedOutput); - - if (result.exitCode !== 0 || hasError) { - this.logAmpxOutput(combinedOutput, context); - const errorMessage = hasError ? 'ampx sandbox failed (found [ERROR] in output)' : `Exit code ${result.exitCode}`; - throw new Error(`ampx sandbox failed: ${errorMessage}`); + if (result.exitCode !== 0) { + throw new Error(`ampx sandbox failed`); } - this.logger.info(`ampx sandbox completed (${durationMs}ms)`, context); + this.logger.info(`ampx sandbox completed (${durationMs}ms)`); // Find the Gen2 root stack by querying CloudFormation // Pattern: amplify---sandbox- @@ -206,36 +175,16 @@ export class Gen2MigrationExecutor { const stackPrefix = `amplify-${deploymentName}-${username}-sandbox`; const gen2StackName = await this.findGen2RootStack(stackPrefix); - this.logger.info(`Gen2 stack name: ${gen2StackName}`, context); + this.logger.info(`Gen2 stack name: ${gen2StackName}`); return gen2StackName; } - /** - * Check ampx output for error indicators. - * ampx sandbox may return exit code 0 even on failure, so we check the output. - */ - private checkForAmpxErrors(output: string): boolean { - // Check for [ERROR] pattern (with or without timestamp prefix) - return output.includes('[ERROR]'); - } - - /** - * Log the last 40 lines of ampx output for debugging. - */ - private logAmpxOutput(output: string, context: LogContext): void { - const outputLines = output.split('\n'); - const last40Lines = outputLines.slice(-40).join('\n'); - this.logger.error(`ampx sandbox output (last 40 lines):\n${last40Lines}`, undefined, context); - } - /** * Find the Gen2 root stack by prefix using AWS CLI. */ private async findGen2RootStack(stackPrefix: string): Promise { - const context: LogContext = { operation: 'find-gen2-stack' }; - - this.logger.debug(`Looking for stack with prefix: ${stackPrefix}`, context); + this.logger.debug(`Looking for stack with prefix: ${stackPrefix}`); const env = this.profile ? { ...process.env, AWS_PROFILE: this.profile } : undefined; diff --git a/packages/amplify-gen2-migration-e2e-system/src/index.ts b/packages/amplify-gen2-migration-e2e-system/src/index.ts index 865aba4959e..e2d605740f4 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/index.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/index.ts @@ -3,9 +3,6 @@ * Exports all public interfaces and classes */ -// Core interfaces -export * from './interfaces'; - // Types export * from './types'; diff --git a/packages/amplify-gen2-migration-e2e-system/src/interfaces/index.ts b/packages/amplify-gen2-migration-e2e-system/src/interfaces/index.ts deleted file mode 100644 index 929ebe2e6f8..00000000000 --- a/packages/amplify-gen2-migration-e2e-system/src/interfaces/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Core interfaces for the Amplify Migration System - */ - -import { AppConfiguration, EnvironmentType, AtmosphereAllocation, ValidationResult, LogLevel, LogContext, CLIOptions } from '../types'; - -// Configuration Management -export interface IConfigurationLoader { - loadAppConfiguration(appName: string): Promise; - validateConfiguration(config: AppConfiguration): ValidationResult; -} - -// Environment Detection and Authentication -export interface IEnvironmentDetector { - detectEnvironment(): Promise; - isAtmosphereEnvironment(): Promise; -} - -// CDK Atmosphere Client Integration -export interface ICDKAtmosphereIntegration { - isAtmosphereEnvironment(): Promise; - initializeForAtmosphere(): Promise; - getProfileFromAllocation(): Promise; - cleanup(): Promise; -} - -// App Selection and Management -export interface IAppSelector { - discoverAvailableApps(): Promise; - validateAppExists(appName: string): Promise; - selectApp(options: CLIOptions): Promise; - getAppPath(appName: string): string; -} - -export interface InitializeAppOptions { - appPath: string; - config: AppConfiguration; - deploymentName: string; - /** Amplify environment name (required, 2-10 lowercase letters) */ - envName: string; - profile: string; -} - -export interface IAppInitializer { - initializeApp(options: InitializeAppOptions): Promise; -} - -// Logging System -export interface ILogger { - debug(message: string, context?: LogContext): void; - info(message: string, context?: LogContext): void; - warn(message: string, context?: LogContext): void; - error(message: string, error?: Error, context?: LogContext): void; - - setLogLevel(level: LogLevel): void; - setLogFilePath(filePath: string): void; -} - -// Utility Interfaces -export interface IFileManager { - readFile(filePath: string): Promise; - writeFile(filePath: string, content: string): Promise; - ensureDirectory(dirPath: string): Promise; - listDirectories(dirPath: string): Promise; - pathExists(filePath: string): Promise; -} - -// AWS Profile Management Types -export interface AWSCredentials { - accessKeyId: string; - secretAccessKey: string; - sessionToken?: string; -} - -export interface AWSProfileData { - credentials: AWSCredentials; - region: string; -} - -// AWS Profile Manager Interface -export interface IAWSProfileManager { - /** - * Writes AWS credentials and config for a named profile - * Creates files with 600 permissions if they don't exist - * Overwrites existing profile if it already exists - */ - writeProfile(profileName: string, profileData: AWSProfileData): Promise; - - /** - * Removes a profile from both credentials and config files - * Completes without error if profile doesn't exist - */ - removeProfile(profileName: string): Promise; -} diff --git a/packages/amplify-gen2-migration-e2e-system/src/types/index.ts b/packages/amplify-gen2-migration-e2e-system/src/types/index.ts index ba08bc1732f..7ba979fcaf0 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/types/index.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/types/index.ts @@ -5,7 +5,6 @@ export interface AppConfiguration { app: AppMetadata; categories: CategoryConfiguration; - dependencies?: DependencyConfiguration; } export interface AppMetadata { @@ -19,7 +18,6 @@ export interface CategoryConfiguration { auth?: AuthConfiguration; storage?: StorageConfiguration; function?: FunctionConfiguration; - hosting?: HostingConfiguration; restApi?: RestApiConfiguration; analytics?: AnalyticsConfiguration; } @@ -96,25 +94,12 @@ export interface FunctionConfiguration { functions: LambdaFunction[]; } -export interface HostingConfiguration { - type: 'amplify-console' | 's3-cloudfront'; - customDomain?: string; - sslCertificate?: string; - buildSettings?: BuildSettings; -} - export interface AnalyticsConfiguration { type: 'kinesis' | 'pinpoint'; name: string; shards?: number; } -export interface DependencyConfiguration { - nodeVersion?: string; - npmPackages?: Record; - amplifyVersion?: string; -} - // Supporting types export type AuthMode = 'API_KEY' | 'COGNITO_USER_POOLS' | 'IAM' | 'OIDC'; export type SignInMethod = 'email' | 'phone' | 'username'; @@ -211,7 +196,6 @@ export interface LogEntry { timestamp: Date; level: LogLevel; message: string; - context?: LogContext; error?: Error; } @@ -222,13 +206,6 @@ export enum LogLevel { ERROR = 'error', } -export interface LogContext { - appName?: string; - category?: string; - step?: string; - operation?: string; -} - // CLI types export interface CLIOptions { app: string; diff --git a/packages/amplify-gen2-migration-e2e-system/src/utils/aws-profile-manager.ts b/packages/amplify-gen2-migration-e2e-system/src/utils/aws-profile-manager.ts index b9b05d8a697..e253c7df8e7 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/utils/aws-profile-manager.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/utils/aws-profile-manager.ts @@ -6,24 +6,33 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import * as os from 'os'; -import { IAWSProfileManager, AWSProfileData, AWSCredentials, ILogger } from '../interfaces'; -import { LogContext } from '../types'; +import { Logger } from './logger'; type IniSections = Record>; -export class AWSProfileManager implements IAWSProfileManager { +export interface AWSCredentials { + accessKeyId: string; + secretAccessKey: string; + sessionToken?: string; +} + +export interface AWSProfileData { + credentials: AWSCredentials; + region: string; +} + +export class AWSProfileManager { private readonly credentialsPath: string; private readonly configPath: string; - constructor(private readonly logger: ILogger, homeDir?: string) { + constructor(private readonly logger: Logger, homeDir?: string) { const home = homeDir || os.homedir(); this.credentialsPath = path.join(home, '.aws', 'credentials'); this.configPath = path.join(home, '.aws', 'config'); } async writeProfile(profileName: string, profileData: AWSProfileData): Promise { - const context: LogContext = { operation: 'writeProfile' }; - this.logger.debug(`Writing profile: ${profileName}`, context); + this.logger.debug(`Writing profile: ${profileName}`); await this.ensureAwsDirectory(); @@ -33,12 +42,11 @@ export class AWSProfileManager implements IAWSProfileManager { // Write config file await this.writeConfigFile(profileName, profileData.region); - this.logger.info(`Successfully wrote profile: ${profileName}`, context); + this.logger.info(`Successfully wrote profile: ${profileName}`); } async removeProfile(profileName: string): Promise { - const context: LogContext = { operation: 'removeProfile' }; - this.logger.debug(`Removing profile: ${profileName}`, context); + this.logger.debug(`Removing profile: ${profileName}`); // Remove from credentials file await this.removeFromCredentialsFile(profileName); @@ -46,7 +54,7 @@ export class AWSProfileManager implements IAWSProfileManager { // Remove from config file await this.removeFromConfigFile(profileName); - this.logger.info(`Successfully removed profile: ${profileName}`, context); + this.logger.info(`Successfully removed profile: ${profileName}`); } private async ensureAwsDirectory(): Promise { @@ -57,8 +65,6 @@ export class AWSProfileManager implements IAWSProfileManager { } private async writeCredentialsFile(profileName: string, credentials: AWSCredentials): Promise { - const context: LogContext = { operation: 'writeCredentialsFile' }; - let sections: IniSections = {}; if (await fs.pathExists(this.credentialsPath)) { @@ -79,12 +85,10 @@ export class AWSProfileManager implements IAWSProfileManager { const serialized = this.serializeIniFile(sections); await this.writeFileWithPermissions(this.credentialsPath, serialized); - this.logger.debug(`Wrote credentials for profile: ${profileName}`, context); + this.logger.debug(`Wrote credentials for profile: ${profileName}`); } private async writeConfigFile(profileName: string, region: string): Promise { - const context: LogContext = { operation: 'writeConfigFile' }; - let sections: IniSections = {}; if (await fs.pathExists(this.configPath)) { @@ -102,14 +106,12 @@ export class AWSProfileManager implements IAWSProfileManager { const serialized = this.serializeIniFile(sections); await this.writeFileWithPermissions(this.configPath, serialized); - this.logger.debug(`Wrote config for profile: ${profileName}`, context); + this.logger.debug(`Wrote config for profile: ${profileName}`); } private async removeFromCredentialsFile(profileName: string): Promise { - const context: LogContext = { operation: 'removeFromCredentialsFile' }; - if (!(await fs.pathExists(this.credentialsPath))) { - this.logger.debug('Credentials file does not exist, nothing to remove', context); + this.logger.debug('Credentials file does not exist, nothing to remove'); return; } @@ -117,7 +119,7 @@ export class AWSProfileManager implements IAWSProfileManager { const sections = this.parseIniFile(content); if (!(profileName in sections)) { - this.logger.debug(`Profile ${profileName} not found in credentials file`, context); + this.logger.debug(`Profile ${profileName} not found in credentials file`); return; } @@ -126,14 +128,12 @@ export class AWSProfileManager implements IAWSProfileManager { const serialized = this.serializeIniFile(sections); await this.writeFileWithPermissions(this.credentialsPath, serialized); - this.logger.debug(`Removed profile ${profileName} from credentials file`, context); + this.logger.debug(`Removed profile ${profileName} from credentials file`); } private async removeFromConfigFile(profileName: string): Promise { - const context: LogContext = { operation: 'removeFromConfigFile' }; - if (!(await fs.pathExists(this.configPath))) { - this.logger.debug('Config file does not exist, nothing to remove', context); + this.logger.debug('Config file does not exist, nothing to remove'); return; } @@ -144,7 +144,7 @@ export class AWSProfileManager implements IAWSProfileManager { const sectionName = profileName === 'default' ? 'default' : `profile ${profileName}`; if (!(sectionName in sections)) { - this.logger.debug(`Profile ${profileName} not found in config file`, context); + this.logger.debug(`Profile ${profileName} not found in config file`); return; } @@ -153,7 +153,7 @@ export class AWSProfileManager implements IAWSProfileManager { const serialized = this.serializeIniFile(sections); await this.writeFileWithPermissions(this.configPath, serialized); - this.logger.debug(`Removed profile ${profileName} from config file`, context); + this.logger.debug(`Removed profile ${profileName} from config file`); } private async writeFileWithPermissions(filePath: string, content: string): Promise { diff --git a/packages/amplify-gen2-migration-e2e-system/src/utils/directory-manager.ts b/packages/amplify-gen2-migration-e2e-system/src/utils/directory-manager.ts index 50ad0fb2efc..304df39236d 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/utils/directory-manager.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/utils/directory-manager.ts @@ -5,8 +5,7 @@ import * as fs from 'fs-extra'; import * as path from 'path'; -import { ILogger } from '../interfaces'; -import { LogContext } from '../types'; +import { Logger } from './logger'; export interface DirectoryCreationOptions { /** Base path where the app directory should be created */ @@ -17,24 +16,14 @@ export interface DirectoryCreationOptions { permissions?: string | number; } -export interface IDirectoryManager { - createAppDirectory(options: DirectoryCreationOptions): Promise; - copyDirectory(source: string, destination: string): Promise; -} - -export class DirectoryManager implements IDirectoryManager { - constructor(private readonly logger: ILogger) {} +export class DirectoryManager { + constructor(private readonly logger: Logger) {} async createAppDirectory(options: DirectoryCreationOptions): Promise { - const context: LogContext = { - appName: options.appName, - operation: 'createAppDirectory', - }; - try { - this.logger.info(`Creating app directory for ${options.appName}`, context); - this.logger.debug(`Base path: ${options.basePath}`, context); - this.logger.debug(`Options: ${JSON.stringify(options, null, 2)}`, context); + this.logger.info(`Creating app directory for ${options.appName}`); + this.logger.debug(`Base path: ${options.basePath}`); + this.logger.debug(`Options: ${JSON.stringify(options, null, 2)}`); // Ensure base path exists (create if needed for temp directories) await fs.ensureDir(options.basePath); @@ -50,15 +39,15 @@ export class DirectoryManager implements IDirectoryManager { // Create the directory await fs.ensureDir(targetPath); - this.logger.debug(`Directory created: ${targetPath}`, context); + this.logger.debug(`Directory created: ${targetPath}`); // Set permissions if specified if (options.permissions !== undefined) { await fs.chmod(targetPath, options.permissions); - this.logger.debug(`Set permissions ${options.permissions} on: ${targetPath}`, context); + this.logger.debug(`Set permissions ${options.permissions} on: ${targetPath}`); } - this.logger.info(`Successfully created app directory: ${targetPath}`, context); + this.logger.info(`Successfully created app directory: ${targetPath}`); return targetPath; } catch (error) { @@ -67,10 +56,8 @@ export class DirectoryManager implements IDirectoryManager { } async copyDirectory(source: string, destination: string): Promise { - const context: LogContext = { operation: 'copyDirectory' }; - try { - this.logger.debug(`Copying directory: ${source} -> ${destination}`, context); + this.logger.debug(`Copying directory: ${source} -> ${destination}`); // Validate source exists and is a directory if (!(await fs.pathExists(source))) { @@ -91,9 +78,13 @@ export class DirectoryManager implements IDirectoryManager { overwrite: false, // Don't overwrite existing files errorOnExist: true, // Throw error if destination exists preserveTimestamps: true, // Preserve file timestamps + // eslint-disable-next-line @typescript-eslint/no-unused-vars + filter: async (src: string, _dest: string) => { + return !src.includes('_snapshot') && !src.includes('node_modules'); + }, }); - this.logger.debug(`Successfully copied directory: ${source} -> ${destination}`, context); + this.logger.debug(`Successfully copied directory: ${source} -> ${destination}`); } catch (error) { throw Error(`Failed to copy directory: ${source} -> ${destination}. Error: ${error}`); } diff --git a/packages/amplify-gen2-migration-e2e-system/src/utils/file-manager.ts b/packages/amplify-gen2-migration-e2e-system/src/utils/file-manager.ts index 94123dccf89..49f54e3152c 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/utils/file-manager.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/utils/file-manager.ts @@ -4,68 +4,59 @@ import * as fs from 'fs-extra'; import * as path from 'path'; -import { IFileManager, ILogger } from '../interfaces'; -import { LogContext } from '../types'; +import { Logger } from './logger'; -export class FileManager implements IFileManager { - constructor(private readonly logger: ILogger) {} +export class FileManager { + constructor(private readonly logger: Logger) {} async readFile(filePath: string): Promise { - const context: LogContext = { operation: 'readFile' }; - try { - this.logger.debug(`Reading file: ${filePath}`, context); + this.logger.debug(`Reading file: ${filePath}`); if (!(await fs.pathExists(filePath))) { throw new Error(`File does not exist: ${filePath}`); } const content = await fs.readFile(filePath, 'utf-8'); - this.logger.debug(`Successfully read file: ${filePath} (${content.length} chars)`, context); + this.logger.debug(`Successfully read file: ${filePath} (${content.length} chars)`); return content; } catch (error) { - this.logger.error(`Failed to read file: ${filePath}`, error as Error, context); + this.logger.error(`Failed to read file: ${filePath}`, error as Error); throw error; } } async writeFile(filePath: string, content: string): Promise { - const context: LogContext = { operation: 'writeFile' }; - try { - this.logger.debug(`Writing file: ${filePath} (${content.length} chars)`, context); + this.logger.debug(`Writing file: ${filePath} (${content.length} chars)`); // Ensure directory exists await this.ensureDirectory(path.dirname(filePath)); await fs.writeFile(filePath, content, 'utf-8'); - this.logger.debug(`Successfully wrote file: ${filePath}`, context); + this.logger.debug(`Successfully wrote file: ${filePath}`); } catch (error) { - this.logger.error(`Failed to write file: ${filePath}`, error as Error, context); + this.logger.error(`Failed to write file: ${filePath}`, error as Error); throw error; } } async ensureDirectory(dirPath: string): Promise { - const context: LogContext = { operation: 'ensureDirectory' }; - try { - this.logger.debug(`Ensuring directory exists: ${dirPath}`, context); + this.logger.debug(`Ensuring directory exists: ${dirPath}`); await fs.ensureDir(dirPath); - this.logger.debug(`Directory ensured: ${dirPath}`, context); + this.logger.debug(`Directory ensured: ${dirPath}`); } catch (error) { - this.logger.error(`Failed to ensure directory: ${dirPath}`, error as Error, context); + this.logger.error(`Failed to ensure directory: ${dirPath}`, error as Error); throw error; } } async listDirectories(dirPath: string): Promise { - const context: LogContext = { operation: 'listDirectories' }; - try { - this.logger.debug(`Listing directories in: ${dirPath}`, context); + this.logger.debug(`Listing directories in: ${dirPath}`); if (!(await fs.pathExists(dirPath))) { throw new Error(`Directory does not exist: ${dirPath}`); @@ -77,10 +68,10 @@ export class FileManager implements IFileManager { .map((entry) => entry.name) .sort(); - this.logger.debug(`Found ${directories.length} directories in: ${dirPath}`, context); + this.logger.debug(`Found ${directories.length} directories in: ${dirPath}`); return directories; } catch (error) { - this.logger.error(`Failed to list directories in: ${dirPath}`, error as Error, context); + this.logger.error(`Failed to list directories in: ${dirPath}`, error as Error); throw error; } } @@ -89,7 +80,7 @@ export class FileManager implements IFileManager { try { return await fs.pathExists(filePath); } catch (error) { - this.logger.debug(`Error checking path existence: ${filePath}`, { operation: 'pathExists' }); + this.logger.debug(`Error checking path existence: ${filePath}`); return false; } } diff --git a/packages/amplify-gen2-migration-e2e-system/src/utils/logger.ts b/packages/amplify-gen2-migration-e2e-system/src/utils/logger.ts index 060cad6d5ce..037a697c34b 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/utils/logger.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/utils/logger.ts @@ -5,31 +5,39 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import chalk from 'chalk'; -import { ILogger } from '../interfaces'; -import { LogLevel, LogContext, LogEntry } from '../types'; +import { LogLevel, LogEntry } from '../types'; -export class Logger implements ILogger { +export class Logger { private logLevel: LogLevel; private logFilePath?: string; + private appName?: string; constructor(logLevel: LogLevel = LogLevel.INFO) { this.logLevel = logLevel; } - debug(message: string, context?: LogContext): void { - this.log(LogLevel.DEBUG, message, undefined, context); + public isDebug(): boolean { + return this.logLevel === LogLevel.DEBUG; } - info(message: string, context?: LogContext): void { - this.log(LogLevel.INFO, message, undefined, context); + debug(message: string): void { + this.log(LogLevel.DEBUG, message, undefined); } - warn(message: string, context?: LogContext): void { - this.log(LogLevel.WARN, message, undefined, context); + info(message: string): void { + this.log(LogLevel.INFO, message, undefined); } - error(message: string, error?: Error, context?: LogContext): void { - this.log(LogLevel.ERROR, message, error, context); + warn(message: string): void { + this.log(LogLevel.WARN, message, undefined); + } + + error(message: string, error?: Error): void { + this.log(LogLevel.ERROR, message, error); + } + + setAppName(appName: string): void { + this.appName = appName; } setLogLevel(level: LogLevel): void { @@ -47,7 +55,7 @@ export class Logger implements ILogger { this.info(`File logging set: ${filePath}`); } - private log(level: LogLevel, message: string, error?: Error, context?: LogContext): void { + private log(level: LogLevel, message: string, error?: Error): void { if (!this.shouldLog(level)) { return; } @@ -56,7 +64,6 @@ export class Logger implements ILogger { timestamp: new Date(), level, message, - context, error, }; @@ -81,24 +88,9 @@ export class Logger implements ILogger { private formatMessage(entry: LogEntry): string { const timestamp = entry.timestamp.toISOString(); const level = this.colorizeLevel(entry.level); - const context = this.formatContext(entry.context); const errorInfo = this.formatError(entry.error); - return `[${timestamp}] ${level}${context} ${entry.message}${errorInfo}`; - } - - private formatContext(context?: LogContext): string { - if (!context) { - return ''; - } - - const parts: string[] = []; - if (context.appName) parts.push(`app:${context.appName}`); - if (context.category) parts.push(`cat:${context.category}`); - if (context.step) parts.push(`step:${context.step}`); - if (context.operation) parts.push(`op:${context.operation}`); - - return parts.length > 0 ? ` [${parts.join('|')}]` : ''; + return `[${timestamp}] ${level} [${this.appName ?? ''}] ${entry.message}${errorInfo}`; } private formatError(error?: Error): string { @@ -117,9 +109,9 @@ export class Logger implements ILogger { case LogLevel.DEBUG: return chalk.gray('[DEBUG]'); case LogLevel.INFO: - return chalk.blue('[INFO] '); + return chalk.blue('[INFO]'); case LogLevel.WARN: - return chalk.yellow('[WARN] '); + return chalk.yellow('[WARN]'); case LogLevel.ERROR: return chalk.red('[ERROR]'); default: diff --git a/packages/amplify-gen2-migration-e2e-system/src/utils/math.ts b/packages/amplify-gen2-migration-e2e-system/src/utils/math.ts index c8d22cab0d6..2f45e534ebb 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/utils/math.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/utils/math.ts @@ -5,7 +5,7 @@ * @param appName Optional app name from which to extract last 8 alphanumeric characters * @returns A unique, sortable app name starting with a letter (max 20 chars) */ -export const generateTimeBasedE2EAmplifyAppName = (appName?: string): string => { +export const generateTimeBasedE2EAmplifyAppName = (appName: string): string => { const now = new Date(); // Format: YYMMDDHHMMSSMM (human-readable, sortable) - 14 chars @@ -15,21 +15,13 @@ export const generateTimeBasedE2EAmplifyAppName = (appName?: string): string => String(now.getDate()).padStart(2, '0'), // DD String(now.getHours()).padStart(2, '0'), // HH String(now.getMinutes()).padStart(2, '0'), // MM - String(now.getSeconds()).padStart(2, '0'), // SS ].join(''); // Extract last 8 alphanumeric characters from appName if provided - // Total: 8 (prefix) + 12 (timestamp) = 20 chars (at 20 char limit) - if (appName) { - const alphanumericOnly = appName.replace(/[^a-zA-Z0-9]/g, ''); - const prefix = alphanumericOnly.slice(-8).toLowerCase(); - if (prefix.length > 0) { - // Ensure prefix starts with a letter to avoid CDK resource naming issues - const safePrefix = /^[a-z]/.test(prefix) ? prefix : `e${prefix.slice(1)}`; - return `${safePrefix}${timestamp}`; - } - } - // if no app name provided, prefix with word starting with alphabetic to avoid CDK resource naming issues - // eslint-disable-next-line spellcheck/spell-checker - return `e2etests${timestamp}`; + // Total: 10 (prefix) + 10 (timestamp) = 20 chars (at 20 char limit) + const alphanumericOnly = appName.replace(/[^a-zA-Z0-9]/g, ''); + const prefix = alphanumericOnly.slice(0, 10).toLowerCase(); + // Ensure prefix starts with a letter to avoid CDK resource naming issues + const safePrefix = /^[a-z]/.test(prefix) ? prefix : `e${prefix.slice(1)}`; + return `${safePrefix}${timestamp}`; };