diff --git a/src/archetypes.ts b/src/archetypes.ts index 4512225..8f84082 100644 --- a/src/archetypes.ts +++ b/src/archetypes.ts @@ -2898,7 +2898,19 @@ function repoDocEnabled( key: "readme" | "contributing" | "security", fallback: boolean ): boolean { - return manifest.repo.docs?.[key] ?? fallback; + if (manifest.repo.docs?.[key] !== undefined) { + return manifest.repo.docs[key]; + } + + // Public repositories need a discoverable vulnerability-reporting route even + // when the manifest predates the v2 docs block. The generated policy uses + // GitHub's private advisory flow and does not add a contact address. An + // explicit `repo.docs.security: false` remains the migration opt-out. + if (key === "security") { + return manifest.project.visibility === "public"; + } + + return fallback; } function envExampleEnabled(manifest: BootstrapManifest): boolean { diff --git a/src/manifest.ts b/src/manifest.ts index a0771e3..84611e3 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -579,7 +579,7 @@ function normalizeRepo(repo: z.input["repo"]): BootstrapM docs: { readme: repo.docs.readme ?? true, contributing: repo.docs.contributing ?? true, - security: repo.docs.security ?? false + ...(repo.docs.security !== undefined ? { security: repo.docs.security } : {}) } } : {}), diff --git a/src/types.ts b/src/types.ts index 7a4f348..cb1a787 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,7 +55,7 @@ export interface RepoConfig { docs?: { readme: boolean; contributing: boolean; - security: boolean; + security?: boolean; }; templates?: { pullRequest: "standard" | "none"; diff --git a/tests/render.test.ts b/tests/render.test.ts index 2c6e384..0fac3ce 100644 --- a/tests/render.test.ts +++ b/tests/render.test.ts @@ -190,6 +190,32 @@ describe("renderManagedFiles", () => { }); + it("projects SECURITY.md for public repositories unless explicitly disabled", () => { + const publicManifest = normalizeManifest({ + project: { name: "public-policy", owner: "acme", visibility: "public" }, + archetype: { kind: "generic-empty" } + }); + expect(renderManagedFiles(publicManifest).some((file) => file.path === "SECURITY.md")).toBe(true); + + const optedOutManifest = normalizeManifest({ + project: { name: "public-policy", owner: "acme", visibility: "public" }, + repo: { docs: { security: false } }, + archetype: { kind: "generic-empty" } + }); + expect(renderManagedFiles(optedOutManifest).some((file) => file.path === "SECURITY.md")).toBe(false); + }); + + it("projects SECURITY.md when public repository docs omit security", () => { + const manifest = normalizeManifest({ + project: { name: "partial-docs-policy", owner: "acme", visibility: "public" }, + repo: { docs: { readme: true } }, + archetype: { kind: "generic-empty" } + }); + + expect(manifest.repo.docs).toEqual({ readme: true, contributing: true }); + expect(renderManagedFiles(manifest).some((file) => file.path === "SECURITY.md")).toBe(true); + }); + it("renders version 2 docs, templates, environment, and workflow switches", () => { const manifest = normalizeManifest({ version: 2,