Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions project.bootstrap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,21 @@ ci:
- release-readiness
nightlyCron: 0 7 * * *
additionalWorkflows: []
dependabot:
enabled: true
securityUpdates: true
versionUpdates: true
ecosystems:
- packageEcosystem: npm
directory: /
interval: weekly
groupMinorAndPatch: true
ignoreMajorUpdates: true
- packageEcosystem: github-actions
directory: /
interval: weekly
groupMinorAndPatch: false
ignoreMajorUpdates: true
aiAttestation:
enabled: true
artifactName: ai-attestation
Expand Down
52 changes: 52 additions & 0 deletions src/archetypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,49 @@ function setupSteps(manifest: BootstrapManifest): string {
return lines.join("\n");
}

function dependabotConfig(manifest: BootstrapManifest): string {
const updates = manifest.ci.dependabot.ecosystems
.map((ecosystem) => {
const lines = [
` - package-ecosystem: "${ecosystem.packageEcosystem}"`,
` directory: "${ecosystem.directory}"`,
" schedule:",
` interval: "${ecosystem.interval}"`
];

if (ecosystem.groupMinorAndPatch) {
lines.push(
" groups:",
` ${ecosystem.packageEcosystem.replace(/[^a-z0-9]+/g, "-")}-minor-patch:`,
" update-types:",
" - \"minor\"",
" - \"patch\""
);
}

if (ecosystem.ignoreMajorUpdates) {
lines.push(
" ignore:",
" - dependency-name: \"*\"",
" update-types:",
" - \"version-update:semver-major\""
);
}

return lines.join("\n");
})
.join("\n\n");

return dedent`
# Generated by OMT Bootstrap. Keep dependency policy in project.bootstrap.yaml.
# Dependabot alerts + security updates are managed through GitHub security settings;
# this file governs routine scheduled version update PRs.
version: 2
updates:
${updates}
`;
}

function aiAttestationCallerWorkflow(manifest: BootstrapManifest): string {
const config = manifest.ci.aiAttestation;
const reusableWorkflow =
Expand Down Expand Up @@ -1691,6 +1734,15 @@ export function renderManagedFiles(manifest: BootstrapManifest): RenderedFile[]
reason: "Extended validation workflow",
contents: `${extendedWorkflow(manifest)}\n`
},
...(manifest.ci.dependabot.enabled && manifest.ci.dependabot.versionUpdates
? [
{
path: ".github/dependabot.yml",
reason: "Dependabot scheduled version update policy",
contents: `${dependabotConfig(manifest)}\n`
}
]
: []),
...(manifest.release.enabled
? [
{
Expand Down
40 changes: 40 additions & 0 deletions src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { z } from "zod";
import { readTextIfExists } from "./lib/fs.js";
import type {
AdditionalWorkflowConfig,
DependabotConfig,
BootstrapManifest,
CodeownerRule,
DefaultRepositoryPermission,
Expand Down Expand Up @@ -114,6 +115,21 @@ const additionalWorkflowSchema = z.object({
purpose: z.string().min(1)
});

const dependabotEcosystemSchema = z.object({
packageEcosystem: z.enum(["npm", "github-actions", "docker"]),
directory: z.string().min(1).optional(),
interval: z.enum(["daily", "weekly", "monthly"]).optional(),
groupMinorAndPatch: z.boolean().optional(),
ignoreMajorUpdates: z.boolean().optional()
});

const dependabotSchema = z.object({
enabled: z.boolean().optional(),
securityUpdates: z.boolean().optional(),
versionUpdates: z.boolean().optional(),
ecosystems: z.array(dependabotEcosystemSchema).optional()
});

const manifestSchema = z.object({
version: z.literal(1).optional(),
project: z.object({
Expand Down Expand Up @@ -172,6 +188,7 @@ const manifestSchema = z.object({
extendedChecks: z.array(z.string()).optional(),
nightlyCron: z.string().optional(),
additionalWorkflows: z.array(additionalWorkflowSchema).optional(),
dependabot: dependabotSchema.optional(),
aiAttestation: z
.object({
enabled: z.boolean().optional(),
Expand Down Expand Up @@ -335,6 +352,28 @@ function normalizeAdditionalWorkflows(
}));
}

function normalizeDependabot(
dependabot: z.input<typeof dependabotSchema> | undefined
): DependabotConfig {
const ecosystems = dependabot?.ecosystems ?? [
{ packageEcosystem: "npm" as const, directory: "/", interval: "weekly" as const },
{ packageEcosystem: "github-actions" as const, directory: "/", interval: "weekly" as const }
];

return {
enabled: dependabot?.enabled ?? true,
securityUpdates: dependabot?.securityUpdates ?? true,
versionUpdates: dependabot?.versionUpdates ?? true,
ecosystems: ecosystems.map((ecosystem) => ({
packageEcosystem: ecosystem.packageEcosystem,
directory: (ecosystem.directory ?? "/").replace(/\\/g, "/"),
interval: ecosystem.interval ?? "weekly",
groupMinorAndPatch: ecosystem.groupMinorAndPatch ?? ecosystem.packageEcosystem === "npm",
ignoreMajorUpdates: ecosystem.ignoreMajorUpdates ?? true
}))
};
}

export function normalizeManifest(raw: z.input<typeof manifestSchema>): BootstrapManifest {
const parsed = manifestSchema.parse(raw);
const reviewers = (parsed.github?.reviewers ?? []).map((reviewer) => reviewer.replace(/^@/, ""));
Expand Down Expand Up @@ -406,6 +445,7 @@ export function normalizeManifest(raw: z.input<typeof manifestSchema>): Bootstra
extendedChecks: parsed.ci?.extendedChecks ?? ["integration", "release-readiness"],
nightlyCron: parsed.ci?.nightlyCron ?? "0 7 * * *",
additionalWorkflows: normalizeAdditionalWorkflows(parsed.ci?.additionalWorkflows),
dependabot: normalizeDependabot(parsed.ci?.dependabot),
aiAttestation: {
enabled: parsed.ci?.aiAttestation?.enabled ?? false,
artifactName: parsed.ci?.aiAttestation?.artifactName ?? "ai-attestation",
Expand Down
16 changes: 16 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ export interface AdditionalWorkflowConfig {
purpose: string;
}

export interface DependabotEcosystemConfig {
packageEcosystem: "npm" | "github-actions" | "docker";
directory: string;
interval: "daily" | "weekly" | "monthly";
groupMinorAndPatch: boolean;
ignoreMajorUpdates: boolean;
}

export interface DependabotConfig {
enabled: boolean;
securityUpdates: boolean;
versionUpdates: boolean;
ecosystems: DependabotEcosystemConfig[];
}

export interface OrganizationSecurityDefaults {
dependabotAlerts: boolean;
dependabotSecurityUpdates: boolean;
Expand Down Expand Up @@ -97,6 +112,7 @@ export interface BootstrapManifest {
extendedChecks: string[];
nightlyCron: string;
additionalWorkflows: AdditionalWorkflowConfig[];
dependabot: DependabotConfig;
aiAttestation: {
enabled: boolean;
artifactName: string;
Expand Down
16 changes: 16 additions & 0 deletions tests/__snapshots__/render.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ exports[`renderManagedFiles > renders a stable managed file set for generic-empt
"executable": false,
"path": ".github/workflows/extended-validation.yml",
},
{
"executable": false,
"path": ".github/dependabot.yml",
},
{
"executable": false,
"path": ".github/workflows/release-tag.yml",
Expand Down Expand Up @@ -143,6 +147,10 @@ exports[`renderManagedFiles > renders a stable managed file set for nextjs-web 1
"executable": false,
"path": ".github/workflows/extended-validation.yml",
},
{
"executable": false,
"path": ".github/dependabot.yml",
},
{
"executable": false,
"path": ".github/workflows/release-tag.yml",
Expand Down Expand Up @@ -248,6 +256,10 @@ exports[`renderManagedFiles > renders a stable managed file set for node-ts-serv
"executable": false,
"path": ".github/workflows/extended-validation.yml",
},
{
"executable": false,
"path": ".github/dependabot.yml",
},
{
"executable": false,
"path": ".github/workflows/release-tag.yml",
Expand Down Expand Up @@ -345,6 +357,10 @@ exports[`renderManagedFiles > renders a stable managed file set for python-servi
"executable": false,
"path": ".github/workflows/extended-validation.yml",
},
{
"executable": false,
"path": ".github/dependabot.yml",
},
{
"executable": false,
"path": ".github/workflows/release-tag.yml",
Expand Down
26 changes: 26 additions & 0 deletions tests/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,17 @@ describe("renderManagedFiles", () => {
expect(prWorkflow?.contents).toContain("PR body must close/link an issue");

const prTemplate = files.find((file) => file.path === ".github/PULL_REQUEST_TEMPLATE.md");
const dependabot = files.find((file) => file.path === ".github/dependabot.yml");
expect(prTemplate?.contents).toContain("## Summary");
expect(prTemplate?.contents).toContain("## Governing Issue");
expect(prTemplate?.contents).toContain("## Validation");
expect(prTemplate?.contents).toContain("## Bootstrap Governance");
expect(prTemplate?.contents).toContain("fallback merge-readiness policy applies");
expect(prTemplate?.contents).toContain("## Notes");
expect(dependabot?.contents).toContain('package-ecosystem: "npm"');
expect(dependabot?.contents).toContain('package-ecosystem: "github-actions"');
expect(dependabot?.contents).toContain("npm-minor-patch");
expect(dependabot?.contents).toContain("version-update:semver-major");

expect(files.some((file) => file.path === "CLAUDE.md")).toBe(false);
expect(files.some((file) => file.path === ".github/workflows/claude.yml")).toBe(false);
Expand All @@ -56,6 +61,27 @@ describe("renderManagedFiles", () => {
});
}

it("can disable Dependabot version update rendering", () => {
const manifest = normalizeManifest({
project: {
name: "quiet-deps",
owner: "acme"
},
archetype: {
kind: "generic-empty"
},
ci: {
dependabot: {
versionUpdates: false
}
}
});

const files = renderManagedFiles(manifest);
expect(files.some((file) => file.path === ".github/dependabot.yml")).toBe(false);
expect(manifest.ci.dependabot.securityUpdates).toBe(true);
});

it("uses the primary required status check name in the generated PR workflow", () => {
const manifest = normalizeManifest({
project: {
Expand Down
Loading