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",