-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Kyle Harding <kyle@balena.io>
- Loading branch information
0 parents
commit 0c35853
Showing
8 changed files
with
3,578 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# local environment settings | ||
.env | ||
|
||
# generated repo files | ||
repos/ | ||
|
||
# NodeJS ignores | ||
node_modules/ | ||
npm-debug.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# probot-get-repo-settings | ||
|
||
Generate repository settings files where they differ from [safe-settings](https://github.com/github/safe-settings) organization defaults. | ||
|
||
This is useful for importing a current snapshot of all repo settings as repo/*.yml files | ||
so it can be enabled org-wide without changing any repo settings. | ||
|
||
A GitHub PAT with admin:read for the org and repos is required. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
--- | ||
# 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 | ||
|
||
branches: | ||
- name: master | ||
protection: | ||
required_pull_request_reviews: | ||
required_approving_review_count: 0 | ||
dismiss_stale_reviews: false | ||
require_code_owner_reviews: false | ||
require_last_push_approval: false | ||
required_status_checks: | ||
strict: true | ||
contexts: | ||
- "policy-bot: master" | ||
enforce_admins: false | ||
restrictions: null | ||
|
||
- name: main | ||
protection: | ||
required_pull_request_reviews: | ||
required_approving_review_count: 0 | ||
dismiss_stale_reviews: false | ||
require_code_owner_reviews: false | ||
require_last_push_approval: false | ||
required_status_checks: | ||
strict: true | ||
contexts: | ||
- "policy-bot: main" | ||
enforce_admins: false | ||
restrictions: null |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
require("dotenv").config(); | ||
const { Probot, ProbotOctokit } = require("probot"); | ||
const { throttling } = require("@octokit/plugin-throttling"); | ||
const yaml = require("yaml"); | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
|
||
// Load the GitHub Personal Access Token and organization name from environment variables | ||
const token = process.env.GITHUB_TOKEN; | ||
const org = process.env.ORG_NAME; | ||
const specificRepo = process.env.SPECIFIC_REPO; // Optionally set this to run on a single repo | ||
const reposDir = "./repos"; | ||
const defaultSettingsPath = "./defaults.yml"; | ||
|
||
// Apply throttling plugin to Octokit | ||
const ThrottledOctokit = ProbotOctokit.plugin(throttling); | ||
|
||
// Initialize a Probot instance with a custom Octokit class | ||
const probot = new Probot({ | ||
appId: 12345, // Dummy App ID, not used in PAT authentication | ||
githubToken: token, | ||
Octokit: ThrottledOctokit.defaults({ | ||
auth: `token ${token}`, | ||
throttle: { | ||
onRateLimit: (retryAfter, options) => { | ||
console.warn( | ||
`Request quota exhausted for request ${options.method} ${options.url}` | ||
); | ||
if (options.request.retryCount === 0) { | ||
// only retries once | ||
console.log(`Retrying after ${retryAfter} seconds!`); | ||
return true; | ||
} | ||
}, | ||
onSecondaryRateLimit: (retryAfter, options) => { | ||
console.warn( | ||
`Secondary rate limit hit for request ${options.method} ${options.url}` | ||
); | ||
}, | ||
}, | ||
}), | ||
}); | ||
|
||
// Read the default settings from the YAML file | ||
const defaultSettings = yaml.parse( | ||
fs.readFileSync(defaultSettingsPath, "utf8") | ||
); | ||
|
||
function flattenEnabledProperty(obj) { | ||
for (const key in obj) { | ||
if (obj.hasOwnProperty(key) && obj[key] && typeof obj[key] === "object") { | ||
if (Object.keys(obj[key]).length === 1 && "enabled" in obj[key]) { | ||
// Flatten to just the 'enabled' value | ||
obj[key] = obj[key].enabled; | ||
} else { | ||
// Recursively process nested objects | ||
flattenEnabledProperty(obj[key]); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function removeSpecificProperties(obj, pathsToRemove) { | ||
pathsToRemove.forEach((path) => { | ||
const parts = path.split("."); | ||
let current = obj; | ||
for (let i = 0; i < parts.length - 1; i++) { | ||
if (current[parts[i]]) { | ||
current = current[parts[i]]; | ||
} else { | ||
return; // Path not found, nothing to remove | ||
} | ||
} | ||
delete current[parts[parts.length - 1]]; | ||
}); | ||
} | ||
|
||
function arraysEqual(arr1, arr2) { | ||
if (arr1.length !== arr2.length) return false; | ||
|
||
const frequencyCounter1 = arr1.reduce((acc, val) => { | ||
acc[val] = (acc[val] || 0) + 1; | ||
return acc; | ||
}, {}); | ||
|
||
const frequencyCounter2 = arr2.reduce((acc, val) => { | ||
acc[val] = (acc[val] || 0) + 1; | ||
return acc; | ||
}, {}); | ||
|
||
for (let key in frequencyCounter1) { | ||
if (frequencyCounter1[key] !== frequencyCounter2[key]) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
function compareObjects(template, actual) { | ||
const diff = {}; | ||
let hasDiff = false; | ||
for (const key in template) { | ||
if (!template.hasOwnProperty(key)) continue; | ||
|
||
// console.error(actual); | ||
|
||
if (Array.isArray(template[key]) && actual[key]) { | ||
// Compare arrays (lists) as unordered | ||
if (!arraysEqual(template[key].sort(), actual[key].sort())) { | ||
diff[key] = actual[key]; | ||
hasDiff = true; | ||
} | ||
} else if ( | ||
typeof template[key] === "object" && | ||
template[key] !== null && | ||
actual[key] | ||
) { | ||
// Deep comparison for objects | ||
const nestedDiff = compareObjects(template[key], actual[key]); | ||
if (nestedDiff) { | ||
diff[key] = nestedDiff; | ||
hasDiff = true; | ||
} | ||
} else if (actual.hasOwnProperty(key) && template[key] !== actual[key]) { | ||
diff[key] = actual[key]; | ||
hasDiff = true; | ||
} | ||
} | ||
return hasDiff ? diff : null; | ||
} | ||
|
||
async function processRepository(octokit, repoName) { | ||
let repoSettingsDiff; | ||
let branchProtectionDiff = []; | ||
|
||
// Fetch repository settings | ||
const repoData = await octokit.rest.repos.get({ owner: org, repo: repoName }); | ||
|
||
if (repoData.data.archived) { | ||
// Skip archived repositories | ||
return; | ||
} | ||
|
||
flattenEnabledProperty(repoData.data); | ||
|
||
repoSettingsDiff = compareObjects(defaultSettings.repository, repoData.data); | ||
|
||
for (const defaultBranch of defaultSettings.branches) { | ||
try { | ||
const protectionData = await octokit.rest.repos.getBranchProtection({ | ||
owner: org, | ||
repo: repoName, | ||
branch: defaultBranch.name, | ||
}); | ||
|
||
// Remove specific properties | ||
removeSpecificProperties(protectionData.data, [ | ||
"required_status_checks.checks", | ||
"required_status_checks.url", | ||
"required_status_checks.contexts_url", | ||
"required_pull_request_reviews.url", | ||
"required_pull_request_reviews.dismissal_restrictions.url", | ||
"required_pull_request_reviews.dismissal_restrictions.teams_url", | ||
"required_pull_request_reviews.dismissal_restrictions.users_url", | ||
"enforce_admins.url", | ||
// Add more paths to remove here as needed | ||
]); | ||
|
||
flattenEnabledProperty(protectionData); | ||
|
||
const protectionOutput = compareObjects( | ||
defaultBranch.protection, | ||
protectionData.data | ||
); | ||
|
||
if (protectionOutput) { | ||
branchProtectionDiff.push({ | ||
name: defaultBranch.name, | ||
protection: protectionOutput, | ||
}); | ||
} | ||
} catch (error) { | ||
// ignore error | ||
} | ||
} | ||
|
||
// Write YAML file if differences are found | ||
if (repoSettingsDiff || branchProtectionDiff.length > 0) { | ||
const jsonData = {}; | ||
if (repoSettingsDiff) { | ||
jsonData.repository = repoSettingsDiff; | ||
} | ||
if (branchProtectionDiff.length > 0) { | ||
jsonData.branches = branchProtectionDiff; | ||
} | ||
const yamlData = yaml.stringify(jsonData); | ||
fs.writeFileSync(`./repos/${repoName}.yml`, yamlData, "utf8"); | ||
} | ||
} | ||
|
||
async function main() { | ||
const octokit = await probot.auth(); | ||
if (!fs.existsSync(reposDir)) { | ||
fs.mkdirSync(reposDir); | ||
} | ||
|
||
if (specificRepo) { | ||
// Process a specific repository | ||
await processRepository(octokit, specificRepo); | ||
} else { | ||
// Process all repositories in the organization | ||
for await (const response of octokit.paginate.iterator( | ||
octokit.rest.repos.listForOrg, | ||
{ | ||
org, | ||
type: "all", | ||
} | ||
)) { | ||
for (const repo of response.data) { | ||
await processRepository(octokit, repo.name); | ||
} | ||
} | ||
} | ||
|
||
console.log("Repository settings processing complete."); | ||
} | ||
|
||
main(); |
Oops, something went wrong.