From 3e2dc3fac3c578efd084d0ebf182900f5e285261 Mon Sep 17 00:00:00 2001 From: Kyle Harding Date: Fri, 23 Aug 2024 15:21:59 -0400 Subject: [PATCH] Mirror current branch protections and rulesets to file Skip any modifications and cleanup of legacy checks and just write the current repository settings to file. This allows methodical cleanup in future PRs. Change-type: patch Signed-off-by: Kyle Harding --- assets/settings.yml | 87 ---------- index.mjs | 377 ++++++++++++++++---------------------------- package-lock.json | 1 + package.json | 4 +- 4 files changed, 134 insertions(+), 335 deletions(-) delete mode 100644 assets/settings.yml diff --git a/assets/settings.yml b/assets/settings.yml deleted file mode 100644 index f63d723..0000000 --- a/assets/settings.yml +++ /dev/null @@ -1,87 +0,0 @@ ---- -# https://github.com/github/safe-settings/blob/main-enterprise/docs/sample-settings/settings.yml -repository: - allow_squash_merge: false - allow_merge_commit: true - allow_rebase_merge: false - allow_auto_merge: true - delete_branch_on_merge: true - -# rulesets can have exceptions for flowzone-app to bypass policies -# https://github.com/github/safe-settings/blob/main-enterprise/docs/sample-settings/org-ruleset.yml -rulesets: - - name: "policy-bot: main" - target: branch - enforcement: active - conditions: - ref_name: - include: - - refs/heads/main - exclude: [] - repository_name: - include: - - ~ALL - exclude: [] - rules: - - type: required_status_checks - parameters: - strict_required_status_checks_policy: true - required_status_checks: - - context: "policy-bot: main" - integration_id: 278558 - - context: "Flowzone / All jobs" - - type: pull_request - parameters: - required_approving_review_count: 0 - dismiss_stale_reviews_on_push: false - require_code_owner_review: false - require_last_push_approval: false - required_review_thread_resolution: false - bypass_actors: - - actor_id: 1 - actor_type: OrganizationAdmin - bypass_mode: always - - actor_id: 5 - actor_type: RepositoryRole - bypass_mode: always - - actor_id: 291899 - actor_type: Integration - bypass_mode: always - - - name: "policy-bot: master" - target: branch - enforcement: active - conditions: - ref_name: - include: - - refs/heads/master - exclude: [] - repository_name: - include: - - ~ALL - exclude: [] - rules: - - type: required_status_checks - parameters: - strict_required_status_checks_policy: true - required_status_checks: - - context: "policy-bot: master" - integration_id: 278558 - - context: "Flowzone / All jobs" - - type: pull_request - parameters: - required_approving_review_count: 0 - dismiss_stale_reviews_on_push: false - require_code_owner_review: false - require_last_push_approval: false - required_review_thread_resolution: false - bypass_actors: - - actor_id: 1 - actor_type: OrganizationAdmin - bypass_mode: always - - actor_id: 5 - actor_type: RepositoryRole - bypass_mode: always - - actor_id: 291899 - actor_type: Integration - bypass_mode: always diff --git a/index.mjs b/index.mjs index 4c924ac..8d5df66 100644 --- a/index.mjs +++ b/index.mjs @@ -12,205 +12,161 @@ const org = process.env.ORG_NAME; const specificRepo = process.env.SPECIFIC_REPO; // Optionally set this to run on a single repo const reposDir = path.join(process.cwd(), ".github", "repos"); -// https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository#using-fnmatch-syntax -const protectedBranches = [ - { - name: "Default", - source: "~DEFAULT_BRANCH", - include: "~DEFAULT_BRANCH", - }, - { - name: "ESR", - source: "2024.7.x", - include: "refs/heads/20[0-9][0-9].*.x", - } -]; - // Initialize Octokit with the GitHub token const octokit = new Octokit({ auth: token }); -async function processRepository(repoName) { - let rulesets = []; - let branches = []; - - // Fetch repository settings - const repoData = await octokit.rest.repos.get({ owner: org, repo: repoName }); - - if (repoData.data.archived) { - // Skip archived repositories - return; - } +function branchRuleToNewBranch(query) { + const branchData = { + name: query.pattern, + protection: { + enforce_admins: query.isAdminEnforced, + required_pull_request_reviews: null, + restrictions: null, + required_status_checks: { + strict: true, + checks: [], + }, + }, + }; - if ([".github"].includes(repoData.data.name)) { - // Skip the .github repository - return; + if (query.requiresStatusChecks) { + branchData.protection.required_status_checks.strict = + query.requiresStrictStatusChecks; + branchData.protection.required_status_checks.checks = + query.requiredStatusCheckContexts; } - console.log(`Processing repository: ${repoName}`); - - for (const branch of protectedBranches) { + return branchData; +} - let protectionData = { - data: { - required_status_checks: { - checks: [], - strict: true, - }, - } - }; - - // Attempt to fetch branch protection data for the provided - // branch name. If the branch protection data is not found, - // attempt to fetch the branch protection data for the default - // branch of the repository. - try { - protectionData = await octokit.rest.repos.getBranchProtection({ - owner: org, - repo: repoName, - branch: branch.source, - }); - } catch (error) { - try { - protectionData = await octokit.rest.repos.getBranchProtection({ - owner: org, - repo: repoName, - branch: repoData.data.default_branch, - }); - } catch (error) { - // ignore errors +async function getBranchProtectionRulesData(owner, repo) { + const query = ` + query($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + branchProtectionRules(first: 100) { + nodes { + pattern + requiresApprovingReviews + requiredApprovingReviewCount + requiresStatusChecks + requiredStatusCheckContexts + requiresStrictStatusChecks + restrictsPushes + restrictsReviewDismissals + isAdminEnforced + } + } } } + `; - const rulesetData = protectionDataToRuleset(protectionData.data, branch); - - // Remove 'policy-bot' contexts and filter out rules without contexts - rulesetData.rules = rulesetData.rules - .map((rule) => { - if (rule.type === "required_status_checks") { - rule.parameters.required_status_checks = - rule.parameters.required_status_checks.filter( - (check) => - !check.context.startsWith("policy-bot") && - !check.context.startsWith("VersionBot") && - !check.context.startsWith("ResinCI") && - !check.context.startsWith("Flowzone") - ); - } - return rule; - }) - .filter( - (rule) => - !( - rule.type === "required_status_checks" && - rule.parameters.required_status_checks.length === 0 - ) - ); - - // Only push to rulesets if there are rules remaining after filters - if (rulesetData.rules.length > 0) { - rulesets.push(rulesetData); - } + try { + const result = await octokit.graphql(query, { + owner: owner, + repo: repo, + }); + + return result.repository.branchProtectionRules.nodes; + } catch (error) { + console.error(error); } +} +async function getRepoRulesetsData(owner, repo) { try { - const repoRulesets = await octokit.rest.repos.getRepoRulesets({ - owner: org, - repo: repoName, - includes_parents: true, + const result = await octokit.rest.repos.getRepoRulesets({ + owner: owner, + repo: repo, + includes_parents: false, }); - repoRulesets.data = repoRulesets.data.filter( - (ruleset) => !ruleset.name.startsWith("policy-bot:") - ); - - for (let i = 0; i < repoRulesets.data.length; ++i) { - const rulesetResponse = await octokit.rest.repos.getRepoRuleset({ - owner: org, - repo: repoName, - ruleset_id: repoRulesets.data[i].id, - }); - - delete rulesetResponse.data.id; - delete rulesetResponse.data.source; - delete rulesetResponse.data.source_type; - delete rulesetResponse.data.created_at; - delete rulesetResponse.data.updated_at; - delete rulesetResponse.data.node_id; - delete rulesetResponse.data.current_user_can_bypass; - delete rulesetResponse.data._links; - - // Filter out context checks starting with Flowzone - rulesetResponse.data.rules = rulesetResponse.data.rules - .map((rule) => { - if (rule.type === "required_status_checks") { - rule.parameters.required_status_checks = - rule.parameters.required_status_checks.filter( - (check) => - !check.context.startsWith("policy-bot") && - !check.context.startsWith("VersionBot") && - !check.context.startsWith("ResinCI") && - !check.context.startsWith("Flowzone") - ); - } - return rule; - }) - .filter( - (rule) => - !( - rule.type === "required_status_checks" && - rule.parameters.required_status_checks.length === 0 - ) - ); - - if (rulesetResponse.data.rules.length < 1) { - // Skip rulesets without rules - continue; - } + return result.data; + } catch (error) { + console.error(error); + } +} - // Skip rulesets without required_status_checks - if ( - !rulesetResponse.data.rules.some( - (rule) => rule.type === "required_status_checks" - ) - ) { - continue; - } +async function getRulesetData(owner, repo, rulesetId) { + try { + const result = await octokit.rest.repos.getRepoRuleset({ + owner: owner, + repo: repo, + ruleset_id: rulesetId, + }); - if (rulesetResponse.data.name === "Default" && rulesetResponse.data.enforcement === "active") { - // Remove branch protection for the default branch if an active ruleset exists - branches.push({name: "default", protection: null}); - }; + return result.data; + } catch (error) { + console.error(error); + } +} - if (rulesetResponse.data.name === "ESR" && rulesetResponse.data.enforcement === "active") { - // Remove branch protection for the ESR branch if an active ruleset exists - branches.push({name: "20*.*", protection: null}); - }; +function rulesetDataToNewRuleset(rulesetData) { + delete rulesetData.id; + delete rulesetData.source; + delete rulesetData.source_type; + delete rulesetData.created_at; + delete rulesetData.updated_at; + delete rulesetData.node_id; + delete rulesetData.current_user_can_bypass; + delete rulesetData._links; + return rulesetData; +} - // Skip existing rulesets with the name "ESR" or "Default" as we are reapplying them - // and duplicates are not allowed - if (["ESR","Default"].includes(rulesetResponse.data.name)) { - continue; - } +async function getRepoData(owner, repo) { + try { + const result = await octokit.rest.repos.get({ + owner: owner, + repo: repo, + }); - rulesets.push(rulesetResponse.data); - } + return result.data; } catch (error) { - // ignore errors console.error(error); } +} + +async function processRepository(repoName) { + let rulesets = []; + let branches = []; - if (rulesets.length < 1) { - // Skip repositories without rulesets + // Fetch repository settings + const repoData = await getRepoData(org, repoName); + + if (repoData.archived) { + // Skip archived repositories return; } + if ([".github"].includes(repoData.name)) { + // Skip the .github repository + return; + } + + console.log(`Processing repository: ${repoName}`); + + const branchRulesData = await getBranchProtectionRulesData(org, repoName); + for (const branchRule of branchRulesData) { + const newBranch = branchRuleToNewBranch(branchRule); + branches.push(newBranch); + } + + const repoRulesetsData = await getRepoRulesetsData(org, repoName); + for (const ruleset of repoRulesetsData) { + const rulesetData = await getRulesetData(org, repoName, ruleset.id); + const newRuleset = rulesetDataToNewRuleset(rulesetData); + rulesets.push(newRuleset); + } + const jsonData = {}; - jsonData.rulesets = rulesets; if (branches.length > 0) { jsonData.branches = branches; } + if (rulesets.length > 0) { + jsonData.rulesets = rulesets; + } + const filePath = path.join(reposDir, `${repoName}.yml`); // Retain any existing props from the repo settings file that are not rulesets or branches @@ -223,11 +179,20 @@ async function processRepository(repoName) { } } + function isEmptyObject(obj) { + return Object.keys(obj).length === 0 && obj.constructor === Object; + } + + // return if jsonData is an empty object + if (isEmptyObject(jsonData)) { + return; + } + const yamlData = yaml.stringify(jsonData); fs.writeFileSync(filePath, yamlData, "utf8"); } -async function fetchRepoSettings() { +async function main() { if (!fs.existsSync(reposDir)) { fs.mkdirSync(reposDir); } @@ -253,82 +218,4 @@ async function fetchRepoSettings() { console.log("Repository settings processing complete."); } -async function main() { - // replace the file at orgSettingsPath with the one bundled with this npm package - // const yamlData = fs.readFileSync(packageSettingsPath, "utf8"); - // fs.writeFileSync(orgSettingsPath, yamlData, "utf8"); - await fetchRepoSettings(); -} - main(); - -function protectionDataToRuleset(protectionData, branch) { - const ruleset = { - name: branch.name, - target: "branch", - enforcement: "active", - conditions: { - ref_name: { - exclude: [], - include: [branch.include], - }, - }, - rules: [], - bypass_actors: [ - { - actor_id: 1, - actor_type: "OrganizationAdmin", - bypass_mode: "always", - }, - { - actor_id: 5, - actor_type: "RepositoryRole", - bypass_mode: "always", - }, - { - actor_id: 291899, - actor_type: "Integration", - bypass_mode: "always", - }, - ], - }; - - // Rule for required status checks - if (protectionData.required_status_checks) { - const statusChecksRule = { - type: "required_status_checks", - parameters: { - // strict_required_status_checks_policy: - // protectionData.required_status_checks.strict, - strict_required_status_checks_policy: true, - required_status_checks: - protectionData.required_status_checks.checks.map((check) => ({ - context: check.context, - })), - }, - }; - ruleset.rules.push(statusChecksRule); - } - - // // Rule for pull request reviews - // if (protectionData.required_pull_request_reviews) { - // const prReviewsRule = { - // type: "pull_request", - // parameters: { - // required_approving_review_count: 0, - // dismiss_stale_reviews_on_push: - // protectionData.required_pull_request_reviews.dismiss_stale_reviews, - // require_code_owner_review: - // protectionData.required_pull_request_reviews - // .require_code_owner_reviews, - // require_last_push_approval: - // protectionData.required_pull_request_reviews - // .require_last_push_approval, - // required_review_thread_resolution: false, // Set as needed - // }, - // }; - // ruleset.rules.push(prReviewsRule); - // } - - return ruleset; -} diff --git a/package-lock.json b/package-lock.json index cd66fca..ee09499 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.31", "license": "Apache-2.0", "dependencies": { + "@octokit/plugin-throttling": "^9.3.1", "dotenv": "^16.3.1", "octokit": "^4.0.2", "rimraf": "^6.0.0", diff --git a/package.json b/package.json index 36737c5..28d4351 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,6 @@ "main": "index.mjs", "bin": "index.mjs", "type": "module", - "files": [ - "assets/settings.yml" - ], "scripts": { "test": "echo \"Error: no test specified\" && exit 0", "start": "npm run clean && node index.mjs", @@ -29,6 +26,7 @@ }, "homepage": "https://github.com/product-os/safe-settings-bootstrap#readme", "dependencies": { + "@octokit/plugin-throttling": "^9.3.1", "dotenv": "^16.3.1", "octokit": "^4.0.2", "rimraf": "^6.0.0",