diff --git a/src/handlers/keys.tsx b/src/handlers/keys.tsx index 295d0ac93..ff9740076 100644 --- a/src/handlers/keys.tsx +++ b/src/handlers/keys.tsx @@ -11,6 +11,13 @@ import { contextKey, globalFlag } from "../router"; // withRegion middleware can fall back to AWS_REGION / the shared config file. export const RegionKey = globalFlag("region", "AWS region", z.string().optional()); +// withRegion overwrites RegionKey with the fully-resolved region (flag -> env -> +// config -> DEFAULT_REGION) before any handler runs, so by the time a leaf reads +// ctx.value(RegionKey) it's never undefined. This key preserves whether --region +// was actually passed, for callers (like the v0 compat path) that must tell +// "user asked for a region" apart from "we picked one for them". +export const RegionExplicitKey = contextKey("region.explicit"); + export const DebugKey = globalFlag("debug", "debug logging", z.boolean().default(false)); export const JsonKey = globalFlag("json", "JSON output", z.boolean().default(false)); diff --git a/src/handlers/project/deploy/index.test.ts b/src/handlers/project/deploy/index.test.ts index 7694de677..c1936fb5d 100644 --- a/src/handlers/project/deploy/index.test.ts +++ b/src/handlers/project/deploy/index.test.ts @@ -273,10 +273,13 @@ describe("project deploy handler", () => { }, ]); expect(subject.io.stdout()).toBe(""); + const regionWarning = + "--region is ignored for v0 projects; v0 uses the target's region from " + + "aws-targets.json, or AWS_REGION / your AWS profile."; expect(subject.io.stderr()).toBe( flag === "--json" - ? "" - : "--region is ignored for v0 projects; v0 resolves region from aws-targets.json.\n" + + ? regionWarning + : `${regionWarning}\n` + "This is a v0 project; running the latest v0 AgentCore CLI deploy in compatibility mode.", ); expect( @@ -285,6 +288,31 @@ describe("project deploy handler", () => { }, ); + test("does not warn about --region when it was never passed", async () => { + const projectRoot = await inProjectWithTargets("[]"); + const specPath = join(projectRoot, "agentcore", "agentcore.json"); + const manifestPath = join(projectRoot, "agentcore", "cdk", "package.json"); + await writeFile(specPath, JSON.stringify({ name: "orders", version: 1 })); + await writeFile( + manifestPath, + JSON.stringify({ dependencies: { "@aws/agentcore-cdk": "^0.1.0-alpha.19" } }), + ); + const legacyDeploys: V0DeployInput[] = []; + const subject = await testDeployCommand({ outputs: {} }, [], { + runV0Deploy: async (input) => { + legacyDeploys.push(input); + return 0; + }, + }); + + await subject.run(["--yes"]); + + expect(legacyDeploys).toHaveLength(1); + expect(subject.io.stderr()).toBe( + "This is a v0 project; running the latest v0 AgentCore CLI deploy in compatibility mode.", + ); + }); + test("preserves the v0 deployer's non-zero exit code", async () => { await inProjectWithTargets(); await writeFile( diff --git a/src/handlers/project/deploy/index.ts b/src/handlers/project/deploy/index.ts index 033473ac0..0ae642447 100644 --- a/src/handlers/project/deploy/index.ts +++ b/src/handlers/project/deploy/index.ts @@ -17,7 +17,7 @@ import { } from "../../../router"; import { JsonRendererKey } from "../../../tui"; import { runWithProgress } from "../../../tui/progress"; -import { JsonKey, RegionKey } from "../../keys"; +import { JsonKey, RegionExplicitKey, RegionKey } from "../../keys"; import { renderJsonError } from "../../utils"; import type { DeployResult, Project, ProjectManager, TeardownConfirmationHandler } from "../types"; @@ -81,8 +81,7 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = recordProjectResourceCounts(ctx, project); const jsonOutput = ctx.require(JsonKey); if (project.legacy) { - warnIfRegionIgnored(config, ctx, jsonOutput); - await delegateV0Deploy(config, { + await delegateV0Deploy(config, ctx, { projectRoot: project.rootPath, target: flags.target, yes: flags.yes, @@ -193,13 +192,11 @@ function withV0Deploy(config: DeployProjectHandlerConfig): Middleware { handle: async (ctx, flags, args) => { const projectRoot = enclosingProjectRoot(process.cwd()); if (projectRoot && (await isV0Project(projectRoot))) { - const jsonOutput = ctx.require(JsonKey); - warnIfRegionIgnored(config, ctx, jsonOutput); - await delegateV0Deploy(config, { + await delegateV0Deploy(config, ctx, { projectRoot, target: flags.target as string, yes: flags.yes as boolean, - json: jsonOutput, + json: ctx.require(JsonKey), }); return; } @@ -208,30 +205,27 @@ function withV0Deploy(config: DeployProjectHandlerConfig): Middleware { }); } -// --region is a v1 global flag that still resolves (and defaults) for every -// command, but v0 deploys have no such flag -- they resolve region entirely -// from aws-targets.json, with env/profile as a fallback. An explicitly passed -// --region is otherwise silently discarded on this path, trading what used to -// be a loud "unknown option" failure for an invisible no-op. -function warnIfRegionIgnored( - config: DeployProjectHandlerConfig, - ctx: Context, - jsonOutput: boolean, -): void { - if (ctx.value(RegionKey) !== undefined && !jsonOutput) { - config.io.stderr.write( - "--region is ignored for v0 projects; v0 resolves region from aws-targets.json.\n", - ); - } -} - async function delegateV0Deploy( config: DeployProjectHandlerConfig, + ctx: Context, input: Parameters[0], ): Promise { // A migration stopped while writing the files leaves a v0 spec beside a v1 CDK // app; the v0 deployer mustn't deploy that. await assertNoPendingMigration(input.projectRoot, input.target); + // --region is a v1 global flag that still resolves (and defaults) for every + // command, but v0 deploys have no such flag -- they resolve region entirely + // from aws-targets.json's saved target, falling back to AWS_REGION or the AWS + // profile. An explicitly passed --region is otherwise silently discarded on + // this path, trading what used to be a loud "unknown option" failure for an + // invisible no-op; warn regardless of --json since stderr is informational, + // not part of the JSON contract. + if (ctx.value(RegionExplicitKey)) { + config.io.stderr.write( + "--region is ignored for v0 projects; v0 uses the target's region from " + + "aws-targets.json, or AWS_REGION / your AWS profile.\n", + ); + } if (!input.json) { config.io.stderr.write( "This is a v0 project; running the latest v0 AgentCore CLI deploy in compatibility mode.\n", diff --git a/src/handlers/project/dev/index.test.ts b/src/handlers/project/dev/index.test.ts index d60063eee..0fe7088dd 100644 --- a/src/handlers/project/dev/index.test.ts +++ b/src/handlers/project/dev/index.test.ts @@ -715,7 +715,9 @@ describe("project dev harnesses", () => { }, ]); expect(subject.deployments).toEqual([]); - expect(subject.io.stderr()).toContain("running the latest v0 AgentCore CLI deploy"); + expect(subject.io.stderr()).toBe( + "This is a v0 project; running the latest v0 AgentCore CLI deploy in compatibility mode.", + ); }); test("harness dev's deploy refuses a target a migration hasn't finished", async () => { diff --git a/src/middleware/withRegion.tsx b/src/middleware/withRegion.tsx index eb305ac3b..e63a75480 100644 --- a/src/middleware/withRegion.tsx +++ b/src/middleware/withRegion.tsx @@ -1,10 +1,12 @@ -import { RegionKey } from "../handlers/keys"; +import { RegionExplicitKey, RegionKey } from "../handlers/keys"; import { resolveRegion } from "../core/region"; import { type Middleware } from "../router"; // withRegion resolves the effective AWS region and pins it onto the context under // RegionKey so the whole app uses a single, consistent value. There is always a -// region: it falls back to DEFAULT_REGION when nothing else is configured. +// region: it falls back to DEFAULT_REGION when nothing else is configured. It also +// records, under RegionExplicitKey, whether the raw --region flag was present +// before this resolution -- the only place that distinction is still observable. export function withRegion(): Middleware { return (h) => ({ name: () => h.name(), @@ -14,8 +16,13 @@ export function withRegion(): Middleware { doesSupportTui: () => h.doesSupportTui(), children: () => h.children(), handle: async (ctx, flags, args) => { + const explicit = ctx.value(RegionKey) !== undefined; const region = await resolveRegion(ctx.value(RegionKey)); - await h.handle(ctx.withValue(RegionKey, region), flags, args); + await h.handle( + ctx.withValue(RegionKey, region).withValue(RegionExplicitKey, explicit), + flags, + args, + ); }, }); }