diff --git a/src/manifest.ts b/src/manifest.ts index d2746c1..96fea15 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -154,6 +154,48 @@ const customScriptsSchema = z.object({ releaseVerification: z.string().min(1).optional() }); +const DEFAULT_RELEASE_CHANGELOG_CATEGORIES = [ + { title: "Features", labels: ["type:feature"] }, + { title: "Fixes", labels: ["type:bug"] }, + { title: "Operations", labels: ["area:infra", "area:qa"] }, + { title: "Documentation", labels: ["kind:docs", "documentation"] } +]; + +const releaseChangelogCategorySchema = z.object({ + title: z.string().min(1), + labels: z.array(z.string().min(1)).min(1) +}); + +const releaseChangelogSchema = z.object({ + enabled: z.boolean().optional(), + mode: z.enum(["github-generated-notes"]).optional(), + categories: z.array(releaseChangelogCategorySchema).optional() +}); + +const releaseVersionSchema = z.object({ + type: z.enum(["npm", "python", "container"]), + path: z.string().min(1) +}); + +const releaseArtifactSchema = z.object({ + directory: z.string().min(1).optional(), + checksum: z.enum(["sha256", "none"]).optional(), + sbom: z.enum(["required", "optional", "disabled"]).optional() +}); + +const releaseContainerPublishSchema = z.object({ + image: z.string().min(1), + updateMajorTag: z.boolean().optional(), + updateMinorTag: z.boolean().optional(), + updateLatestTag: z.boolean().optional() +}); + +const releasePublishSchema = z.object({ + githubReleaseAssets: z.boolean().optional(), + packages: z.array(z.string().min(1)).optional(), + containers: z.array(releaseContainerPublishSchema).optional() +}); + const manifestSchema = z.object({ version: z.literal(1).optional(), project: z.object({ @@ -240,7 +282,11 @@ const manifestSchema = z.object({ updateMajorTag: z.boolean().optional(), updateMinorTag: z.boolean().optional(), reusableWorkflowRepo: z.string().min(1).optional(), - reusableWorkflowRef: z.string().min(1).optional() + reusableWorkflowRef: z.string().min(1).optional(), + changelog: releaseChangelogSchema.optional(), + versions: z.array(releaseVersionSchema).optional(), + artifacts: releaseArtifactSchema.optional(), + publish: releasePublishSchema.optional() }) .optional(), agents: z @@ -524,7 +570,29 @@ export function normalizeManifest(raw: z.input): Bootstra updateMajorTag: parsed.release?.updateMajorTag ?? true, updateMinorTag: parsed.release?.updateMinorTag ?? true, reusableWorkflowRepo: parsed.release?.reusableWorkflowRepo ?? `${parsed.project.owner}/bootstrap`, - reusableWorkflowRef: parsed.release?.reusableWorkflowRef ?? "refs/heads/main" + reusableWorkflowRef: parsed.release?.reusableWorkflowRef ?? "refs/heads/main", + changelog: { + enabled: parsed.release?.changelog?.enabled ?? true, + mode: parsed.release?.changelog?.mode ?? "github-generated-notes", + categories: parsed.release?.changelog?.categories ?? DEFAULT_RELEASE_CHANGELOG_CATEGORIES + }, + versions: parsed.release?.versions ?? [], + artifacts: { + directory: parsed.release?.artifacts?.directory ?? "dist/release", + checksum: parsed.release?.artifacts?.checksum ?? "sha256", + sbom: parsed.release?.artifacts?.sbom ?? "optional" + }, + publish: { + githubReleaseAssets: parsed.release?.publish?.githubReleaseAssets ?? true, + packages: parsed.release?.publish?.packages ?? [], + containers: + parsed.release?.publish?.containers?.map((container) => ({ + image: container.image, + updateMajorTag: container.updateMajorTag ?? true, + updateMinorTag: container.updateMinorTag ?? true, + updateLatestTag: container.updateLatestTag ?? false + })) ?? [] + } }, agents: { manageCodexHome: parsed.agents?.manageCodexHome ?? true, diff --git a/src/types.ts b/src/types.ts index 0c8d2b7..7bbf484 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,6 +58,46 @@ export interface CustomScriptsConfig { releaseVerification?: string; } +export type ReleaseChangelogMode = "github-generated-notes"; +export type ReleaseVersionType = "npm" | "python" | "container"; +export type ReleaseChecksumType = "sha256" | "none"; +export type ReleaseSbomMode = "required" | "optional" | "disabled"; + +export interface ReleaseChangelogCategory { + title: string; + labels: string[]; +} + +export interface ReleaseChangelogConfig { + enabled: boolean; + mode: ReleaseChangelogMode; + categories: ReleaseChangelogCategory[]; +} + +export interface ReleaseVersionSurface { + type: ReleaseVersionType; + path: string; +} + +export interface ReleaseArtifactConfig { + directory: string; + checksum: ReleaseChecksumType; + sbom: ReleaseSbomMode; +} + +export interface ReleaseContainerPublishConfig { + image: string; + updateMajorTag: boolean; + updateMinorTag: boolean; + updateLatestTag: boolean; +} + +export interface ReleasePublishConfig { + githubReleaseAssets: boolean; + packages: string[]; + containers: ReleaseContainerPublishConfig[]; +} + export interface OrganizationSecurityDefaults { dependabotAlerts: boolean; dependabotSecurityUpdates: boolean; @@ -150,6 +190,10 @@ export interface BootstrapManifest { updateMinorTag: boolean; reusableWorkflowRepo: string; reusableWorkflowRef: string; + changelog: ReleaseChangelogConfig; + versions: ReleaseVersionSurface[]; + artifacts: ReleaseArtifactConfig; + publish: ReleasePublishConfig; }; agents: { manageCodexHome: boolean; diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts index 73fc439..63f9fa7 100644 --- a/tests/manifest.test.ts +++ b/tests/manifest.test.ts @@ -45,7 +45,28 @@ describe("normalizeManifest", () => { updateMajorTag: true, updateMinorTag: true, reusableWorkflowRepo: "acme/bootstrap", - reusableWorkflowRef: "refs/heads/main" + reusableWorkflowRef: "refs/heads/main", + changelog: { + enabled: true, + mode: "github-generated-notes", + categories: [ + { title: "Features", labels: ["type:feature"] }, + { title: "Fixes", labels: ["type:bug"] }, + { title: "Operations", labels: ["area:infra", "area:qa"] }, + { title: "Documentation", labels: ["kind:docs", "documentation"] } + ] + }, + versions: [], + artifacts: { + directory: "dist/release", + checksum: "sha256", + sbom: "optional" + }, + publish: { + githubReleaseAssets: true, + packages: [], + containers: [] + } }); expect(manifest.environments.stage.reviewers).toEqual(["alice", "acme/platform"]); expect(manifest.environments.prod.branches).toEqual(["main"]); @@ -206,7 +227,30 @@ describe("normalizeManifest", () => { createGitHubRelease: false, updateMajorTag: true, updateMinorTag: false, - reusableWorkflowRef: "refs/tags/v1" + reusableWorkflowRef: "refs/tags/v1", + changelog: { + enabled: false, + categories: [{ title: "Infrastructure", labels: ["area:infra"] }] + }, + versions: [ + { type: "npm", path: "package.json" }, + { type: "python", path: "pyproject.toml" } + ], + artifacts: { + directory: "build/release", + checksum: "none", + sbom: "disabled" + }, + publish: { + githubReleaseAssets: false, + packages: ["npm"], + containers: [ + { + image: "ghcr.io/omt-global/release-repo", + updateLatestTag: true + } + ] + } } }); @@ -217,7 +261,67 @@ describe("normalizeManifest", () => { updateMajorTag: true, updateMinorTag: false, reusableWorkflowRepo: "OMT-Global/bootstrap", - reusableWorkflowRef: "refs/tags/v1" + reusableWorkflowRef: "refs/tags/v1", + changelog: { + enabled: false, + mode: "github-generated-notes", + categories: [{ title: "Infrastructure", labels: ["area:infra"] }] + }, + versions: [ + { type: "npm", path: "package.json" }, + { type: "python", path: "pyproject.toml" } + ], + artifacts: { + directory: "build/release", + checksum: "none", + sbom: "disabled" + }, + publish: { + githubReleaseAssets: false, + packages: ["npm"], + containers: [ + { + image: "ghcr.io/omt-global/release-repo", + updateMajorTag: true, + updateMinorTag: true, + updateLatestTag: true + } + ] + } + }); + }); + + it("normalizes release automation extension defaults", () => { + const manifest = normalizeManifest({ + project: { + name: "release-repo", + owner: "OMT-Global" + }, + archetype: { + kind: "generic-empty" + } + }); + + expect(manifest.release.changelog).toEqual({ + enabled: true, + mode: "github-generated-notes", + categories: [ + { title: "Features", labels: ["type:feature"] }, + { title: "Fixes", labels: ["type:bug"] }, + { title: "Operations", labels: ["area:infra", "area:qa"] }, + { title: "Documentation", labels: ["kind:docs", "documentation"] } + ] + }); + expect(manifest.release.versions).toEqual([]); + expect(manifest.release.artifacts).toEqual({ + directory: "dist/release", + checksum: "sha256", + sbom: "optional" + }); + expect(manifest.release.publish).toEqual({ + githubReleaseAssets: true, + packages: [], + containers: [] }); });