diff --git a/cspell.json b/cspell.json index 49b380cbf..0a9089663 100644 --- a/cspell.json +++ b/cspell.json @@ -17,6 +17,8 @@ "infile", "joshuakgoldberg", "markdownlintignore", - "mtfoley" + "mtfoley", + "ruleset", + "rulesets" ] } diff --git a/docs/Tooling.md b/docs/Tooling.md index 180ded8a2..6a1a75abe 100644 --- a/docs/Tooling.md +++ b/docs/Tooling.md @@ -133,7 +133,7 @@ In code, assorted repository documentation files for GitHub are created: On the GitHub repository, metadata will be populated: - [Issue labels](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels) for issue areas, statuses, and types. -- [Repository settings](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features) such as [branch protections](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) and [squash merging PRs](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges) +- [Repository settings](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features) such as [branch rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository) and [squash merging PRs](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges) ### Type Checking diff --git a/src/next/blocks/blockRepositoryBranchProtection.ts b/src/next/blocks/blockRepositoryBranchProtection.ts deleted file mode 100644 index 5291ac603..000000000 --- a/src/next/blocks/blockRepositoryBranchProtection.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { z } from "zod"; - -import { base } from "../base.js"; - -export const blockRepositoryBranchProtection = base.createBlock({ - about: { - name: "Repository Branch Protection", - }, - addons: { - requiredStatusChecks: z.array(z.string()).default([]), - }, - produce({ addons, options }) { - return { - requests: [ - { - id: "branch-protection", - async send({ octokit }) { - await octokit.request( - `PUT /repos/{owner}/{repository}/branches/{main}/protection`, - { - allow_deletions: false, - allow_force_pushes: true, - allow_fork_pushes: false, - allow_fork_syncing: true, - block_creations: false, - branch: "main", - enforce_admins: false, - owner: options.owner, - repo: options.repository, - required_conversation_resolution: true, - required_linear_history: false, - required_pull_request_reviews: null, - required_status_checks: { - checks: addons.requiredStatusChecks.map((context) => ({ - context, - })), - strict: false, - }, - restrictions: null, - }, - ); - }, - }, - ], - }; - }, -}); diff --git a/src/next/blocks/blockRepositoryBranchProtection.test.ts b/src/next/blocks/blockRepositoryBranchRuleset.test.ts similarity index 68% rename from src/next/blocks/blockRepositoryBranchProtection.test.ts rename to src/next/blocks/blockRepositoryBranchRuleset.test.ts index bef44aa99..6c731f39e 100644 --- a/src/next/blocks/blockRepositoryBranchProtection.test.ts +++ b/src/next/blocks/blockRepositoryBranchRuleset.test.ts @@ -1,12 +1,12 @@ import { testBlock } from "create-testers"; import { describe, expect, test } from "vitest"; -import { blockRepositoryBranchProtection } from "./blockRepositoryBranchProtection.js"; +import { blockRepositoryBranchRuleset } from "./blockRepositoryBranchRuleset.js"; import { optionsBase } from "./options.fakes.js"; -describe("blockRepositoryBranchProtection", () => { +describe("blockRepositoryBranchRuleset", () => { test("without addons", () => { - const creation = testBlock(blockRepositoryBranchProtection, { + const creation = testBlock(blockRepositoryBranchRuleset, { options: optionsBase, }); @@ -14,7 +14,7 @@ describe("blockRepositoryBranchProtection", () => { { "requests": [ { - "id": "branch-protection", + "id": "branch-ruleset", "send": [Function], }, ], @@ -26,7 +26,7 @@ describe("blockRepositoryBranchProtection", () => { // https://github.com/JoshuaKGoldberg/create/issues/65 test("with addons", () => { - const creation = testBlock(blockRepositoryBranchProtection, { + const creation = testBlock(blockRepositoryBranchRuleset, { addons: { requiredStatusChecks: ["build", "test"], }, @@ -37,7 +37,7 @@ describe("blockRepositoryBranchProtection", () => { { "requests": [ { - "id": "branch-protection", + "id": "branch-ruleset", "send": [Function], }, ], diff --git a/src/next/blocks/blockRepositoryBranchRuleset.ts b/src/next/blocks/blockRepositoryBranchRuleset.ts new file mode 100644 index 000000000..efdff96eb --- /dev/null +++ b/src/next/blocks/blockRepositoryBranchRuleset.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; + +import { base } from "../base.js"; + +export const blockRepositoryBranchRuleset = base.createBlock({ + about: { + name: "Repository Branch Ruleset", + }, + addons: { + requiredStatusChecks: z.array(z.string()).default([]), + }, + produce({ addons, options }) { + return { + requests: [ + { + id: "branch-ruleset", + async send({ octokit }) { + await octokit.request("POST /repos/{owner}/{repo}/rulesets", { + conditions: { + ref_name: { + exclude: [], + include: ["refs/heads/main"], + }, + }, + enforcement: "active", + name: "Branch protection for main", + owner: options.owner, + repo: options.repository, + rules: [ + { type: "deletion" }, + { + parameters: { + // @ts-expect-error -- https://github.com/github/rest-api-description/issues/4405 + allowed_merge_methods: ["squash"], + dismiss_stale_reviews_on_push: false, + require_code_owner_review: false, + require_last_push_approval: false, + required_approving_review_count: 0, + required_review_thread_resolution: false, + }, + type: "pull_request", + }, + { + parameters: { + required_status_checks: addons.requiredStatusChecks.map( + (context) => ({ context }), + ), + strict_required_status_checks_policy: false, + }, + type: "required_status_checks", + }, + ], + target: "branch", + }); + }, + }, + ], + }; + }, +}); diff --git a/src/next/presetMinimal.ts b/src/next/presetMinimal.ts index af182b917..a7baca4ea 100644 --- a/src/next/presetMinimal.ts +++ b/src/next/presetMinimal.ts @@ -13,7 +13,7 @@ import { blockMITLicense } from "./blocks/blockMITLicense.js"; import { blockPackageJson } from "./blocks/blockPackageJson.js"; import { blockPrettier } from "./blocks/blockPrettier.js"; import { blockREADME } from "./blocks/blockREADME.js"; -import { blockRepositoryBranchProtection } from "./blocks/blockRepositoryBranchProtection.js"; +import { blockRepositoryBranchRuleset } from "./blocks/blockRepositoryBranchRuleset.js"; import { blockRepositoryLabels } from "./blocks/blockRepositoryLabels.js"; import { blockRepositorySettings } from "./blocks/blockRepositorySettings.js"; import { blockTemplatedBy } from "./blocks/blockTemplatedBy.js"; @@ -41,7 +41,7 @@ export const presetMinimal = base.createPreset({ blockPackageJson, blockPrettier, blockREADME, - blockRepositoryBranchProtection, + blockRepositoryBranchRuleset, blockRepositoryLabels, blockRepositorySettings, blockTemplatedBy, diff --git a/src/steps/initializeBranchProtectionSettings.test.ts b/src/steps/initializeBranchProtectionSettings.test.ts index afff91b9d..3e097e1ca 100644 --- a/src/steps/initializeBranchProtectionSettings.test.ts +++ b/src/steps/initializeBranchProtectionSettings.test.ts @@ -37,53 +37,74 @@ describe("migrateBranchProtectionSettings", () => { expect(mockRequest.mock.calls).toMatchInlineSnapshot(` [ [ - "PUT /repos///branches/main/protection", + "POST /repos/{owner}/{repo}/rulesets", { - "allow_deletions": false, - "allow_force_pushes": true, - "allow_fork_pushes": false, - "allow_fork_syncing": true, - "block_creations": false, - "branch": "main", - "enforce_admins": false, + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "refs/heads/main", + ], + }, + }, + "enforcement": "active", + "name": "Branch protection for main", "owner": "", "repo": "", - "required_conversation_resolution": true, - "required_linear_history": false, - "required_pull_request_reviews": null, - "required_status_checks": { - "checks": [ - { - "context": "Build", - }, - { - "context": "Compliance", - }, - { - "context": "Lint", - }, - { - "context": "Lint Knip", - }, - { - "context": "Lint Markdown", - }, - { - "context": "Lint Packages", + "rules": [ + { + "type": "deletion", + }, + { + "parameters": { + "allowed_merge_methods": [ + "squash", + ], + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_approving_review_count": 0, + "required_review_thread_resolution": false, }, - { - "context": "Lint Spelling", + "type": "pull_request", + }, + { + "parameters": { + "required_status_checks": [ + { + "context": "Build", + }, + { + "context": "Compliance", + }, + { + "context": "Lint", + }, + { + "context": "Lint Knip", + }, + { + "context": "Lint Markdown", + }, + { + "context": "Lint Packages", + }, + { + "context": "Lint Spelling", + }, + { + "context": "Prettier", + }, + { + "context": "Test", + }, + ], + "strict_required_status_checks_policy": false, }, - { - "context": "Prettier", - }, - { - "context": "Test", - }, - ], - "strict": false, - }, - "restrictions": null, + "type": "required_status_checks", + }, + ], + "target": "branch", }, ], ] @@ -129,35 +150,56 @@ describe("migrateBranchProtectionSettings", () => { expect(mockRequest.mock.calls).toMatchInlineSnapshot(` [ [ - "PUT /repos///branches/main/protection", + "POST /repos/{owner}/{repo}/rulesets", { - "allow_deletions": false, - "allow_force_pushes": true, - "allow_fork_pushes": false, - "allow_fork_syncing": true, - "block_creations": false, - "branch": "main", - "enforce_admins": false, + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "refs/heads/main", + ], + }, + }, + "enforcement": "active", + "name": "Branch protection for main", "owner": "", "repo": "", - "required_conversation_resolution": true, - "required_linear_history": false, - "required_pull_request_reviews": null, - "required_status_checks": { - "checks": [ - { - "context": "Build", - }, - { - "context": "Lint", + "rules": [ + { + "type": "deletion", + }, + { + "parameters": { + "allowed_merge_methods": [ + "squash", + ], + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_approving_review_count": 0, + "required_review_thread_resolution": false, }, - { - "context": "Prettier", + "type": "pull_request", + }, + { + "parameters": { + "required_status_checks": [ + { + "context": "Build", + }, + { + "context": "Lint", + }, + { + "context": "Prettier", + }, + ], + "strict_required_status_checks_policy": false, }, - ], - "strict": false, - }, - "restrictions": null, + "type": "required_status_checks", + }, + ], + "target": "branch", }, ], ] diff --git a/src/steps/initializeGitHubRepository/initializeBranchProtectionSettings.ts b/src/steps/initializeGitHubRepository/initializeBranchProtectionSettings.ts index ada242128..cbb817b07 100644 --- a/src/steps/initializeGitHubRepository/initializeBranchProtectionSettings.ts +++ b/src/steps/initializeGitHubRepository/initializeBranchProtectionSettings.ts @@ -8,42 +8,55 @@ export async function initializeBranchProtectionSettings( options: Options, ) { try { - await octokit.request( - `PUT /repos/${options.owner}/${options.repository}/branches/main/protection`, - { - allow_deletions: false, - allow_force_pushes: true, - allow_fork_pushes: false, - allow_fork_syncing: true, - block_creations: false, - branch: "main", - enforce_admins: false, - owner: options.owner, - repo: options.repository, - required_conversation_resolution: true, - required_linear_history: false, - required_pull_request_reviews: null, - required_status_checks: { - checks: [ - { context: "Build" }, - ...(options.excludeCompliance ? [] : [{ context: "Compliance" }]), - { context: "Lint" }, - ...(options.excludeLintKnip ? [] : [{ context: "Lint Knip" }]), - ...(options.excludeLintMd ? [] : [{ context: "Lint Markdown" }]), - ...(options.excludeLintPackages - ? [] - : [{ context: "Lint Packages" }]), - ...(options.excludeLintSpelling - ? [] - : [{ context: "Lint Spelling" }]), - { context: "Prettier" }, - ...(options.excludeTests ? [] : [{ context: "Test" }]), - ], - strict: false, + await octokit.request("POST /repos/{owner}/{repo}/rulesets", { + conditions: { + ref_name: { + exclude: [], + include: ["refs/heads/main"], }, - restrictions: null, }, - ); + enforcement: "active", + name: "Branch protection for main", + owner: options.owner, + repo: options.repository, + rules: [ + { type: "deletion" }, + { + parameters: { + // @ts-expect-error -- https://github.com/github/rest-api-description/issues/4405 + allowed_merge_methods: ["squash"], + dismiss_stale_reviews_on_push: false, + require_code_owner_review: false, + require_last_push_approval: false, + required_approving_review_count: 0, + required_review_thread_resolution: false, + }, + type: "pull_request", + }, + { + parameters: { + required_status_checks: [ + { context: "Build" }, + ...(options.excludeCompliance ? [] : [{ context: "Compliance" }]), + { context: "Lint" }, + ...(options.excludeLintKnip ? [] : [{ context: "Lint Knip" }]), + ...(options.excludeLintMd ? [] : [{ context: "Lint Markdown" }]), + ...(options.excludeLintPackages + ? [] + : [{ context: "Lint Packages" }]), + ...(options.excludeLintSpelling + ? [] + : [{ context: "Lint Spelling" }]), + { context: "Prettier" }, + ...(options.excludeTests ? [] : [{ context: "Test" }]), + ], + strict_required_status_checks_policy: false, + }, + type: "required_status_checks", + }, + ], + target: "branch", + }); } catch (error) { if ((error as RequestError).status === 403) { return false;