diff --git a/.changeset/cyan-impalas-protect.md b/.changeset/cyan-impalas-protect.md new file mode 100644 index 00000000000..3d3dd1d5da8 --- /dev/null +++ b/.changeset/cyan-impalas-protect.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-cli': patch +--- + +stackname validation now allows customers to point to nested stack diff --git a/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts b/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts index b6aa2065d76..4496559364f 100644 --- a/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts +++ b/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts @@ -2,7 +2,10 @@ import { beforeEach, describe, it, mock } from 'node:test'; import { GenerateOutputsCommand } from './generate_outputs_command.js'; import { ClientConfigFormat } from '@aws-amplify/client-config'; import yargs, { CommandModule } from 'yargs'; -import { TestCommandRunner } from '../../../test-utils/command_runner.js'; +import { + TestCommandError, + TestCommandRunner, +} from '../../../test-utils/command_runner.js'; import assert from 'node:assert'; import { AppBackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js'; import { ClientConfigGeneratorAdapter } from '../../../client-config/client_config_generator_adapter.js'; @@ -82,6 +85,37 @@ void describe('generate outputs command', () => { ); }); + void it('generates and writes config for stack with slashes', async () => { + await commandRunner.runCommand( + 'outputs --stack parent/child --out-dir /foo/bar' + ); + assert.equal(generateClientConfigMock.mock.callCount(), 1); + assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], { + stackName: 'parent/child', + }); + }); + + void it('throws an error for invalid stack name', async () => { + await assert.rejects( + commandRunner.runCommand('outputs --stack 1invalid --out-dir /foo/bar'), + (error: TestCommandError) => { + assert.strictEqual(error.error.name, 'InvalidStackNameError'); + assert.strictEqual(error.error.message, 'Invalid stack name: 1invalid'); + return true; + } + ); + }); + + void it('accepts valid stack name with hyphens', async () => { + await commandRunner.runCommand( + 'outputs --stack valid-stack-name --out-dir /foo/bar' + ); + assert.equal(generateClientConfigMock.mock.callCount(), 1); + assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], { + stackName: 'valid-stack-name', + }); + }); + void it('generates and writes config for branch', async () => { await commandRunner.runCommand( 'outputs --branch branch_name --out-dir /foo/bar' diff --git a/packages/cli/src/commands/generate/outputs/generate_outputs_command.ts b/packages/cli/src/commands/generate/outputs/generate_outputs_command.ts index 0edb427a00e..a9d8bfe0b9d 100644 --- a/packages/cli/src/commands/generate/outputs/generate_outputs_command.ts +++ b/packages/cli/src/commands/generate/outputs/generate_outputs_command.ts @@ -55,6 +55,17 @@ export class GenerateOutputsCommand handler = async ( args: ArgumentsCamelCase ): Promise => { + if (args.stack) { + const identifierRegex = /^[a-zA-Z][-_a-zA-Z0-9/]*$/; + if (!args.stack.match(identifierRegex)) { + throw new AmplifyUserError('InvalidStackNameError', { + message: `Invalid stack name: ${args.stack}`, + resolution: + 'Stack name must start with a letter and can only contain alphanumeric characters, hyphens, underscores and slashes.', + }); + } + } + const backendIdentifier = await this.backendIdentifierResolver.resolveDeployedBackendIdentifier( args