diff --git a/src/handlers/eval/config-bundle/config-bundle.test.tsx b/src/handlers/eval/config-bundle/config-bundle.test.tsx index 1e9172fa1..d7c260e97 100644 --- a/src/handlers/eval/config-bundle/config-bundle.test.tsx +++ b/src/handlers/eval/config-bundle/config-bundle.test.tsx @@ -10,7 +10,7 @@ import { testIO, } from "../../../testing"; import { createRootHandler } from "../../index"; -import type { UpdateConfigurationBundleInput } from "../types"; +import type { CreateConfigurationBundleInput, UpdateConfigurationBundleInput } from "../types"; const REGION = "us-west-2"; const COMPONENT_ARN = @@ -97,7 +97,7 @@ describe("eval config-bundle command hierarchy", () => { .find((child) => child.name() === "create") ?.flags() .map((candidate) => candidate.name), - ).toEqual(["name", "components", "kms-key-arn"]); + ).toEqual(["name", "components", "branch-name", "commit-message", "kms-key-arn"]); expect( configBundle ?.children() @@ -164,6 +164,34 @@ describe("eval config-bundle command hierarchy", () => { }); describe("config-bundle create", () => { + test("passes branch name, commit message, and KMS key", async () => { + const { core, route } = testConfigBundleCommand(); + + await route([ + "eval", + "config-bundle", + "create", + "--name", + "orders-prompt", + "--components", + JSON.stringify(COMPONENTS), + "--branch-name", + "feature/order-routing", + "--commit-message", + "Add order routing configuration", + "--kms-key-arn", + "arn:aws:kms:us-west-2:123456789012:key/initial", + ]); + + expect(callArgs(core, "createConfigurationBundle")[0]).toEqual({ + bundleName: "orders-prompt", + components: COMPONENTS, + branchName: "feature/order-routing", + commitMessage: "Add order routing configuration", + kmsKeyArn: "arn:aws:kms:us-west-2:123456789012:key/initial", + } satisfies CreateConfigurationBundleInput); + }); + test("reads components from stdin", async () => { const { core, route } = testConfigBundleCommand(JSON.stringify(COMPONENTS)); @@ -182,6 +210,25 @@ describe("config-bundle create", () => { }); }); + test("rejects a branch name that does not match service constraints", async () => { + const { core, route } = testConfigBundleCommand(); + + await expect( + route([ + "eval", + "config-bundle", + "create", + "--name", + "orders-prompt", + "--components", + JSON.stringify(COMPONENTS), + "--branch-name", + "1-invalid", + ]), + ).rejects.toThrow(/Invalid value for option '--branch-name'/); + expect(core.eval.calls).toHaveLength(0); + }); + test.each([ ["an empty map", {}], ["a component without configuration", { [COMPONENT_ARN]: {} }], diff --git a/src/handlers/eval/config-bundle/create/index.tsx b/src/handlers/eval/config-bundle/create/index.tsx index 784596468..098a4c231 100644 --- a/src/handlers/eval/config-bundle/create/index.tsx +++ b/src/handlers/eval/config-bundle/create/index.tsx @@ -7,6 +7,13 @@ import type { Core } from "../../../types"; import { coreOptsFromCtx } from "../../../utils"; import { resolveConfigurationBundleComponents } from "../components"; +const BranchNameSchema = z + .string() + .min(1) + .max(128) + .regex(/^[a-zA-Z][a-zA-Z0-9_/-]{0,127}$/, "Value must match [a-zA-Z][a-zA-Z0-9_/-]"); +const CommitMessageSchema = z.string().max(500); + export const createCreateConfigBundleHandler = (core: Core, io: AppIO) => createHandler({ name: "create", @@ -19,6 +26,12 @@ export const createCreateConfigBundleHandler = (core: Core, io: AppIO) => z.string().optional(), { sensitive: true }, ), + flag("branch-name", "branch name for the initial configuration", BranchNameSchema.optional()), + flag( + "commit-message", + "message describing the initial configuration", + CommitMessageSchema.optional(), + ), flag( "kms-key-arn", "customer managed KMS key ARN for component configurations", @@ -42,6 +55,8 @@ export const createCreateConfigBundleHandler = (core: Core, io: AppIO) => { bundleName: flags["name"], components, + branchName: flags["branch-name"], + commitMessage: flags["commit-message"], kmsKeyArn: flags["kms-key-arn"], }, coreOptsFromCtx(ctx), diff --git a/src/handlers/eval/types.tsx b/src/handlers/eval/types.tsx index 278f8f34c..226c17f81 100644 --- a/src/handlers/eval/types.tsx +++ b/src/handlers/eval/types.tsx @@ -185,7 +185,7 @@ export type RoleScopeWarning = { export type CreateDatasetInput = CreateDatasetRequest; export type CreateConfigurationBundleInput = Pick< CreateConfigurationBundleRequest, - "bundleName" | "components" | "kmsKeyArn" + "bundleName" | "components" | "branchName" | "commitMessage" | "kmsKeyArn" >; export type UpdateConfigurationBundleInput = Required< Pick diff --git a/src/projectSchemas/config-bundle.test.ts b/src/projectSchemas/config-bundle.test.ts new file mode 100644 index 000000000..71edf0d60 --- /dev/null +++ b/src/projectSchemas/config-bundle.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "bun:test"; +import { ConfigBundleBranchNameSchema } from "./config-bundle"; + +describe("ConfigBundleBranchNameSchema", () => { + test.each(["mainline", "feature/order-routing", "release-2026_08"])( + "accepts service-compatible branch name %s", + (branchName) => { + expect(ConfigBundleBranchNameSchema.safeParse(branchName).success).toBe(true); + }, + ); + + test.each(["", "1-mainline", "feature branch", "feature.order"])( + "rejects service-incompatible branch name %s", + (branchName) => { + expect(ConfigBundleBranchNameSchema.safeParse(branchName).success).toBe(false); + }, + ); + + test("enforces the service's 128-character maximum", () => { + expect(ConfigBundleBranchNameSchema.safeParse(`a${"b".repeat(127)}`).success).toBe(true); + expect(ConfigBundleBranchNameSchema.safeParse(`a${"b".repeat(128)}`).success).toBe(false); + }); +}); diff --git a/src/projectSchemas/config-bundle.ts b/src/projectSchemas/config-bundle.ts index 74a219124..d32baf351 100644 --- a/src/projectSchemas/config-bundle.ts +++ b/src/projectSchemas/config-bundle.ts @@ -15,7 +15,11 @@ export const ComponentConfigurationSchema = z.object({ export type ComponentConfiguration = z.infer; export const ComponentConfigurationMapSchema = z.record(z.string(), ComponentConfigurationSchema); export type ComponentConfigurationMap = z.infer; -export const ConfigBundleBranchNameSchema = z.string().max(128); +export const ConfigBundleBranchNameSchema = z + .string() + .min(1) + .max(128) + .regex(/^[a-zA-Z][a-zA-Z0-9_/-]{0,127}$/, "Value must match [a-zA-Z][a-zA-Z0-9_/-]"); export const ConfigBundleCommitMessageSchema = z.string().max(500); export const ConfigBundleSchema = z.object({ name: ConfigBundleNameSchema,