diff --git a/amplify-migration-apps/product-catalog/configure.sh b/amplify-migration-apps/product-catalog/configure.sh index b4f6510f709..9890f8e9d7f 100755 --- a/amplify-migration-apps/product-catalog/configure.sh +++ b/amplify-migration-apps/product-catalog/configure.sh @@ -2,11 +2,12 @@ set -euxo pipefail +api_name=$(ls amplify/backend/api) s3_trigger_function_name=$(ls amplify/backend/function | grep S3Trigger) -cp -f schema.graphql ./amplify/backend/api/productcatalog/schema.graphql +cp -f schema.graphql ./amplify/backend/api/${api_name}/schema.graphql cp -f lowstockproducts.js ./amplify/backend/function/lowstockproducts/src/index.js cp -f lowstockproducts.package.json ./amplify/backend/function/lowstockproducts/src/package.json cp -f onimageuploaded.js ./amplify/backend/function/${s3_trigger_function_name}/src/index.js cp -f onimageuploaded.package.json ./amplify/backend/function/${s3_trigger_function_name}/src/package.json -cp -f custom-roles.json ./amplify/backend/api/productcatalog/custom-roles.json \ No newline at end of file +cp -f custom-roles.json ./amplify/backend/api/${api_name}/custom-roles.json diff --git a/amplify-migration-apps/product-catalog/lowstockproducts.js b/amplify-migration-apps/product-catalog/lowstockproducts.js index 23653856e33..dfa5928f3eb 100644 --- a/amplify-migration-apps/product-catalog/lowstockproducts.js +++ b/amplify-migration-apps/product-catalog/lowstockproducts.js @@ -6,7 +6,10 @@ const { SSMClient, GetParametersCommand } = require('@aws-sdk/client-ssm'); const Sha256 = crypto.Sha256; -const GRAPHQL_ENDPOINT = process.env.API_PRODUCTCATALOG_GRAPHQLAPIENDPOINTOUTPUT; +const GRAPHQL_ENDPOINT = Object.keys(process.env) + .filter((k) => k.startsWith('API_') && k.endsWith('_GRAPHQLAPIENDPOINTOUTPUT')) + .map((k) => process.env[k]) + .find(Boolean); const AWS_REGION = process.env.AWS_REGION || 'us-east-1'; const LOW_STOCK_THRESHOLD = parseInt(process.env.LOW_STOCK_THRESHOLD) || 5; diff --git a/amplify-migration-apps/product-catalog/migration-config.json b/amplify-migration-apps/product-catalog/migration-config.json index c2b9ac3e0b9..9038b053938 100644 --- a/amplify-migration-apps/product-catalog/migration-config.json +++ b/amplify-migration-apps/product-catalog/migration-config.json @@ -35,7 +35,16 @@ { "name": "lowstockproducts", "runtime": "nodejs", - "template": "hello-world" + "template": "hello-world", + "environment": { + "LOW_STOCK_THRESHOLD": "5" + }, + "secrets": { + "PRODUCT_CATALOG_SECRET": "product-catalog-secret-value" + }, + "apiAccess": { + "operations": ["Query"] + } } ] }, diff --git a/amplify-migration-apps/product-catalog/post-generate.ts b/amplify-migration-apps/product-catalog/post-generate.ts new file mode 100644 index 00000000000..a5c30c611ea --- /dev/null +++ b/amplify-migration-apps/product-catalog/post-generate.ts @@ -0,0 +1,200 @@ +#!/usr/bin/env npx ts-node +/** + * Post-generate script for product-catalog app. + * + * Applies manual edits required after `amplify gen2-migration generate`: + * 1. Convert lowstockproducts function from CommonJS to ESM + update secret fetching + * 2. Convert S3 trigger function from CommonJS to ESM + * 3. Update lowstockproducts/resource.ts to use secret() for PRODUCT_CATALOG_SECRET + * 4. Update frontend import from amplifyconfiguration.json to amplify_outputs.json + */ + +import fs from 'fs/promises'; +import path from 'path'; + +interface PostGenerateOptions { + appPath: string; + envName?: string; +} + +async function convertLowStockToESM(appPath: string): Promise { + const handlerPath = path.join(appPath, 'amplify', 'function', 'lowstockproducts', 'index.js'); + + console.log(`Converting lowstockproducts to ESM in ${handlerPath}...`); + + let content: string; + try { + content = await fs.readFile(handlerPath, 'utf-8'); + } catch { + console.log(' index.js not found, skipping'); + return; + } + + // Convert exports.handler to ESM export + let updated = content.replace( + /exports\.handler\s*=\s*async\s*\((\w*)\)\s*=>\s*\{/g, + 'export async function handler($1) {', + ); + + // Replace SSM secret fetching with env var read: + // const secretValue = await fetchSecret(); + // becomes: + // const secretValue = process.env['PRODUCT_CATALOG_SECRET']; + updated = updated.replace( + /const secretValue = await fetchSecret\(\);/g, + "const secretValue = process.env['PRODUCT_CATALOG_SECRET'];", + ); + + if (updated === content) { + console.log(' No changes needed, skipping'); + return; + } + + await fs.writeFile(handlerPath, updated, 'utf-8'); + console.log(' Converted to ESM and updated secret fetching'); +} + +async function convertS3TriggerToESM(appPath: string): Promise { + // Find the S3 trigger function directory (name varies per deployment) + const storagePath = path.join(appPath, 'amplify', 'storage'); + + let triggerDirs: string[]; + try { + const entries = await fs.readdir(storagePath, { withFileTypes: true }); + triggerDirs = entries + .filter((e) => e.isDirectory() && e.name.startsWith('S3Trigger')) + .map((e) => e.name); + } catch { + console.log(' amplify/storage/ not found, skipping'); + return; + } + + for (const triggerDir of triggerDirs) { + const handlerPath = path.join(storagePath, triggerDir, 'index.js'); + + console.log(`Converting ${triggerDir} to ESM in ${handlerPath}...`); + + let content: string; + try { + content = await fs.readFile(handlerPath, 'utf-8'); + } catch { + console.log(' index.js not found, skipping'); + continue; + } + + // Convert exports.handler = async function (event) { to export async function handler(event) { + let updated = content.replace( + /exports\.handler\s*=\s*async\s*function\s*\((\w*)\)\s*\{/g, + 'export async function handler($1) {', + ); + + // Also handle arrow function pattern + updated = updated.replace( + /exports\.handler\s*=\s*async\s*\((\w*)\)\s*=>\s*\{/g, + 'export async function handler($1) {', + ); + + if (updated === content) { + console.log(' No CommonJS exports found, skipping'); + continue; + } + + await fs.writeFile(handlerPath, updated, 'utf-8'); + console.log(' Converted to ESM syntax'); + } +} + + +async function updateLowStockResourceTs(appPath: string): Promise { + const resourcePath = path.join(appPath, 'amplify', 'function', 'lowstockproducts', 'resource.ts'); + + console.log(`Updating lowstockproducts/resource.ts to use secret()...`); + + let content: string; + try { + content = await fs.readFile(resourcePath, 'utf-8'); + } catch { + console.log(' resource.ts not found, skipping'); + return; + } + + // Add secret import if not present + let updated = content.replace( + /import \{ defineFunction \} from ["']@aws-amplify\/backend["'];/, + 'import { defineFunction, secret } from "@aws-amplify/backend";', + ); + + // Replace the SSM path with secret() call + // The generated code has something like: + // PRODUCT_CATALOG_SECRET: "/amplify/..." + // Replace with: + // PRODUCT_CATALOG_SECRET: secret("PRODUCT_CATALOG_SECRET") + updated = updated.replace( + /PRODUCT_CATALOG_SECRET:\s*\n?\s*['"][^'"]+['"]/, + 'PRODUCT_CATALOG_SECRET: secret("PRODUCT_CATALOG_SECRET")', + ); + + if (updated === content) { + console.log(' No changes needed, skipping'); + return; + } + + await fs.writeFile(resourcePath, updated, 'utf-8'); + console.log(' Updated to use secret()'); +} + +async function updateFrontendConfig(appPath: string): Promise { + const mainPath = path.join(appPath, 'src', 'main.tsx'); + + console.log(`Updating frontend config import in ${mainPath}...`); + + let content: string; + try { + content = await fs.readFile(mainPath, 'utf-8'); + } catch { + console.log(' main.tsx not found, skipping'); + return; + } + + // Change: import amplifyconfig from './amplifyconfiguration.json'; + // To: import amplifyconfig from '../amplify_outputs.json'; + const updated = content.replace( + /from\s*["']\.\/amplifyconfiguration\.json["']/g, + "from '../amplify_outputs.json'", + ); + + if (updated === content) { + console.log(' No amplifyconfiguration.json import found, skipping'); + return; + } + + await fs.writeFile(mainPath, updated, 'utf-8'); + console.log(' Updated import to amplify_outputs.json'); +} + +export async function postGenerate(options: PostGenerateOptions): Promise { + const { appPath } = options; + + console.log(`Running post-generate for product-catalog at ${appPath}`); + console.log(''); + + await convertLowStockToESM(appPath); + await convertS3TriggerToESM(appPath); + await updateLowStockResourceTs(appPath); + await updateFrontendConfig(appPath); + + console.log(''); + console.log('Post-generate completed'); +} + +// CLI entry point +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { + const appPath = process.argv[2] || process.cwd(); + const envName = process.argv[3] || 'main'; + + postGenerate({ appPath, envName }).catch((error) => { + console.error('Post-generate failed:', error); + process.exit(1); + }); +} diff --git a/amplify-migration-apps/product-catalog/post-refactor.ts b/amplify-migration-apps/product-catalog/post-refactor.ts new file mode 100644 index 00000000000..da59391abeb --- /dev/null +++ b/amplify-migration-apps/product-catalog/post-refactor.ts @@ -0,0 +1,70 @@ +#!/usr/bin/env npx ts-node +/** + * Post-refactor script for product-catalog app. + * + * Applies manual edits required after `amplify gen2-migration refactor`: + * 1. Uncomment s3Bucket.bucketName in amplify/backend.ts to preserve the original bucket name. + */ + +import fs from 'fs/promises'; +import path from 'path'; + +interface PostRefactorOptions { + appPath: string; + envName?: string; +} + +async function uncommentBucketName(appPath: string): Promise { + const backendPath = path.join(appPath, 'amplify', 'backend.ts'); + + console.log(`Uncommenting s3Bucket.bucketName in ${backendPath}...`); + + let content: string; + try { + content = await fs.readFile(backendPath, 'utf-8'); + } catch { + console.log(' backend.ts not found, skipping'); + return; + } + + // The generated code has: + // // s3Bucket.bucketName = '...'; + // Uncomment it: + // s3Bucket.bucketName = '...'; + const updated = content.replace( + /\/\/\s*(s3Bucket\.bucketName\s*=\s*['"][^'"]+['"];)/, + '$1', + ); + + if (updated === content) { + console.log(' No commented bucketName found, skipping'); + return; + } + + await fs.writeFile(backendPath, updated, 'utf-8'); + console.log(' Uncommented s3Bucket.bucketName'); +} + +export async function postRefactor(options: PostRefactorOptions): Promise { + const { appPath } = options; + + console.log(`Running post-refactor for product-catalog at ${appPath}`); + console.log(''); + + await uncommentBucketName(appPath); + + console.log(''); + console.log('Post-refactor completed'); +} + +// CLI entry point +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { + const appPath = process.argv[2] || process.cwd(); + const envName = process.argv[3] || 'main'; + + postRefactor({ appPath, envName }).catch((error) => { + console.error('Post-refactor failed:', error); + process.exit(1); + }); +} 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 789689f8e67..b92e5b5ad50 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 @@ -30,6 +30,7 @@ import { addKinesis, updateSchema, } from '@aws-amplify/amplify-e2e-core'; +import type { CoreFunctionSettings } from '@aws-amplify/amplify-e2e-core'; import * as fs from 'fs'; import * as path from 'path'; import { Logger } from '../utils/logger'; @@ -54,7 +55,6 @@ export class CategoryInitializer { */ async initializeCategories(options: CategoryInitializerOptions): Promise { const { appPath, config, deploymentName } = options; - const result: InitializeCategoriesResult = { initializedCategories: [], skippedCategories: [], @@ -69,14 +69,15 @@ export class CategoryInitializer { return result; } - // Initialize categories in the correct order: + // Initialize categories in the order matching the manual CLI workflow: // 1. Auth first (other categories may depend on it) // 2. Analytics before functions (functions may reference analytics resources) - // 3. Regular functions (non-trigger) before API + // 3. GraphQL API (must exist before functions that need API access) // 4. Storage (may have triggers that reference functions) - // 5. GraphQL API (creates AppSync tables that trigger functions may reference) + // 5. Regular functions (non-trigger) after API so they can request API access // 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); } @@ -85,17 +86,17 @@ export class CategoryInitializer { await this.initializeAnalyticsCategory(appPath, categories.analytics, result); } - // Initialize regular (non-trigger) functions before API - if (categories.function) { - await this.initializeRegularFunctions(appPath, categories.function, result); + if (categories.api) { + await this.initializeApiCategory(appPath, categories.api, categories.function, result); } if (categories.storage) { await this.initializeStorageCategory(appPath, categories.storage, categories.auth, result); } - if (categories.api) { - await this.initializeApiCategory(appPath, categories.api, categories.function, result); + // Initialize regular (non-trigger) functions after API so they can request API access + if (categories.function) { + await this.initializeRegularFunctions(appPath, categories.function, result); } // Initialize trigger functions after API (they need AppSync tables to exist) @@ -109,7 +110,6 @@ export class CategoryInitializer { } this.logger.info(`Category initialization complete. Initialized: ${result.initializedCategories.join(', ') || 'none'}`); - return result; } @@ -349,6 +349,7 @@ export class CategoryInitializer { // Check if guest access is configured for any bucket const hasGuestAccess = storageConfig.buckets.some((bucket) => bucket.access.includes('guest') || bucket.access.includes('public')); + // Check if triggers are configured const hasTriggers = storageConfig.triggers && storageConfig.triggers.length > 0; @@ -438,7 +439,9 @@ export class CategoryInitializer { } /** - * Initialize regular (non-trigger) Lambda functions + * Initialize regular (non-trigger) Lambda functions. + * Passes additionalPermissions, environmentVariables, and secretsConfig + * when the migration config specifies apiAccess, environment, or secrets. */ private async initializeRegularFunctions( appPath: string, @@ -457,18 +460,60 @@ export class CategoryInitializer { try { for (const func of regularFunctions) { this.logger.debug(`Adding function: ${func.name}`); - const runtime = this.mapRuntime(func.runtime); const template = this.mapTemplate(func.template); - await addFunction( - appPath, - { - name: func.name, - functionTemplate: template, - }, - runtime, - ); + // Build settings matching the manual CLI prompts from the README + const settings: CoreFunctionSettings = { + name: func.name, + functionTemplate: template, + }; + + // API access: "Do you want to access other resources?" → Yes → api → operations + if (func.apiAccess) { + const apiName = this.getApiNameFromBackend(appPath); + if (apiName) { + const operations = func.apiAccess.operations; // e.g. ['Query'] + // The choices list must match the order the CLI presents categories. + // The CLI reads Object.keys(amplifyMeta) filtered to exclude 'providers' and 'predictions'. + // We read the actual category order from amplify-meta.json to ensure correct multiSelect positioning. + const categoryChoices = this.getCategoryChoicesFromMeta(appPath); + settings.additionalPermissions = { + permissions: ['api'], + choices: categoryChoices, + resources: [apiName], + operations, + }; + this.logger.debug( + `Function ${func.name}: API access configured (${operations.join(', ')}) with choices: [${categoryChoices.join(', ')}]`, + ); + } else { + this.logger.warn(`Function ${func.name} has apiAccess but no API found in backend`); + } + } + + // Environment variables: "Do you want to configure environment variables?" → Yes + if (func.environment && Object.keys(func.environment).length > 0) { + const firstKey = Object.keys(func.environment)[0]; + settings.environmentVariables = { + key: firstKey, + value: func.environment[firstKey], + }; + this.logger.debug(`Function ${func.name}: env var ${firstKey}=${func.environment[firstKey]}`); + } + + // Secrets: "Do you want to configure secret values?" → Yes + if (func.secrets && Object.keys(func.secrets).length > 0) { + const firstSecretName = Object.keys(func.secrets)[0]; + settings.secretsConfig = { + operation: 'add', + name: firstSecretName, + value: func.secrets[firstSecretName], + }; + this.logger.debug(`Function ${func.name}: secret ${firstSecretName} configured`); + } + + await addFunction(appPath, settings, runtime); this.logger.debug(`Function ${func.name} added successfully`); } @@ -503,7 +548,6 @@ export class CategoryInitializer { try { for (const func of triggerFunctions) { this.logger.debug(`Adding trigger function: ${func.name}`); - const runtime = this.mapRuntime(func.runtime); const triggerType = func.trigger?.type; @@ -608,6 +652,23 @@ export class CategoryInitializer { } } + /** + * Read category keys from amplify-meta.json in the order the CLI presents them. + * The CLI uses Object.keys(amplifyMeta) filtered to exclude 'providers' and 'predictions'. + */ + private getCategoryChoicesFromMeta(appPath: string): string[] { + try { + const metaPath = path.join(appPath, 'amplify', 'backend', 'amplify-meta.json'); + if (fs.existsSync(metaPath)) { + const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')) as Record; + return Object.keys(meta).filter((k) => k !== 'providers' && k !== 'predictions'); + } + return ['api', 'auth', 'function', 'storage']; + } catch { + return ['api', 'auth', 'function', 'storage']; + } + } + /** * Initialize the analytics category * Supports: Kinesis Data Streams 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 deleted file mode 100644 index d5e87a5f986..00000000000 --- a/packages/amplify-gen2-migration-e2e-system/src/core/gen2-migration-executor.ts +++ /dev/null @@ -1,231 +0,0 @@ -/** - * Gen2 Migration Executor - * - * Executes gen2-migration CLI commands (lock, generate, refactor) - * from the amplify-cli package to migrate Gen1 apps to Gen2. - */ - -import execa from 'execa'; -import os from 'os'; -import { getCLIPath } from '@aws-amplify/amplify-e2e-core'; -import { Logger } from '../utils/logger'; - -/** - * Available gen2-migration steps - */ -export type Gen2MigrationStep = 'lock' | 'generate' | 'refactor'; - -/** - * Options for Gen2MigrationExecutor - */ -export interface Gen2MigrationExecutorOptions { - /** AWS profile to use for CLI commands */ - profile?: string; -} - -/** - * Executor for gen2-migration CLI commands. - * - * The migration workflow consists of: - * 1. lock - Lock the Gen1 environment to prevent updates during migration - * 2. generate - Generate Gen2 code from Gen1 configuration - * 3. refactor - Move stateful resources from Gen1 to Gen2 stacks - */ -export class Gen2MigrationExecutor { - private readonly amplifyPath: string; - private readonly profile?: string; - - constructor(private readonly logger: Logger, options?: Gen2MigrationExecutorOptions) { - this.amplifyPath = getCLIPath(true); - this.profile = options?.profile; - } - - /** - * Execute a gen2-migration step. Throws on failure. - */ - private async executeStep(step: Gen2MigrationStep, appPath: string, extraArgs: string[] = []): Promise { - 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(' ')}`); - - const startTime = Date.now(); - - // Set AWS_PROFILE env var if profile is specified - const env = this.profile ? { ...process.env, AWS_PROFILE: this.profile } : undefined; - - const result = await execa(this.amplifyPath, args, { - cwd: appPath, - stdio: 'inherit', - reject: false, - env, - }); - - const durationMs = Date.now() - startTime; - - if (result.exitCode !== 0) { - const errorMessage = result.stderr || result.stdout || `Exit code ${result.exitCode}`; - 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)`); - } - - /** - * Lock the Gen1 environment. - * - * Enables deletion protection on DynamoDB tables, sets a deny-all stack policy, - * and adds GEN2_MIGRATION_ENVIRONMENT_NAME env var to the Amplify app. - */ - public async lock(appPath: string): Promise { - await this.executeStep('lock', appPath); - } - - /** - * Generate Gen2 code from Gen1 configuration. - * - * Creates/updates package.json with Gen2 dependencies, replaces the amplify - * folder with Gen2 TypeScript definitions, and installs dependencies. - */ - public async generate(appPath: string): Promise { - await this.executeStep('generate', appPath); - this.logger.info('Installing dependencies..'); - await execa('npm', ['install'], { cwd: appPath }); - } - - /** - * Move stateful resources from Gen1 to Gen2 stacks. - * - * Requires Gen2 deployment to be complete before running. - */ - public async refactor(appPath: string, gen2StackName: string): Promise { - await this.executeStep('refactor', appPath, ['--to', gen2StackName]); - } - - /** - * Run pre-deployment workflow: lock -> checkout gen2 branch -> generate - */ - public async runPreDeploymentWorkflow(appPath: string, envName = 'main'): Promise { - 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}'...`); - 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'); - } - - /** - * Deploy Gen2 app using ampx sandbox. - * - * Runs `npx ampx sandbox --once` to do a single non-interactive deployment. - * Returns the Gen2 root stack name by querying CloudFormation. - * - * @param appPath - Path to the app directory - * @param deploymentName - Unique deployment name for stack identification - * @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 { - 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(); - - // Set AWS_PROFILE and AWS_BRANCH env vars - // AWS_BRANCH ensures unique Lambda function names across sandbox deployments - const env: NodeJS.ProcessEnv = { - ...process.env, - AWS_BRANCH: branchName, - ...(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, - }); - - const durationMs = Date.now() - startTime; - - if (result.exitCode !== 0) { - throw new Error(`ampx sandbox failed`); - } - - this.logger.info(`ampx sandbox completed (${durationMs}ms)`); - - // Find the Gen2 root stack by querying CloudFormation - // Pattern: amplify---sandbox- - const username = os.userInfo().username; - const stackPrefix = `amplify-${deploymentName}-${username}-sandbox`; - - const gen2StackName = await this.findGen2RootStack(stackPrefix); - this.logger.info(`Gen2 stack name: ${gen2StackName}`); - - return gen2StackName; - } - - /** - * Find the Gen2 root stack by prefix using AWS CLI. - */ - private async findGen2RootStack(stackPrefix: string): Promise { - this.logger.debug(`Looking for stack with prefix: ${stackPrefix}`); - - const env = this.profile ? { ...process.env, AWS_PROFILE: this.profile } : undefined; - - const result = await execa( - 'aws', - [ - 'cloudformation', - 'list-stacks', - '--stack-status-filter', - 'CREATE_COMPLETE', - 'UPDATE_COMPLETE', - '--query', - `StackSummaries[?starts_with(StackName, '${stackPrefix}')].StackName`, - '--output', - 'text', - ], - { reject: false, env }, - ); - - if (result.exitCode !== 0) { - throw new Error(`Failed to list CloudFormation stacks: ${result.stderr || result.stdout}`); - } - - const stacks = result.stdout - .trim() - .split(/\s+/) - .filter((s) => s.length > 0); - - // Find root stacks (those without nested stack suffixes like -auth, -data, -storage) - const rootStacks = stacks.filter((name) => { - const suffix = name.replace(stackPrefix, ''); - // Root stack has pattern: - (10 char hex) - // Nested stacks have pattern: --- - return /^-[a-f0-9]+$/.test(suffix); - }); - - if (rootStacks.length === 0) { - throw new Error(`No Gen2 sandbox stack found with prefix: ${stackPrefix}`); - } - - // Return the most recently created (should only be one) - return rootStacks[0]; - } -} 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 7ba979fcaf0..c0d9ecd4173 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/types/index.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/types/index.ts @@ -137,6 +137,10 @@ export interface LambdaFunction { environment?: Record; permissions?: string[]; trigger?: FunctionTrigger; + secrets?: Record; + apiAccess?: { + operations: string[]; + }; } export interface FunctionTrigger {