Tool: sails run freeze-open-pull-requests (#5610)

* trivial (comments)

* Remove old unfreeze/freeze logic

* trivial (clarify comment)

* Trivial (fix weird character)

* Extrapolate DRI mappings into config.

* Explain why this exists

* Extrapolate logic

* Use extrapolated logic + add 5 second wait time to prevent accidents + clean up

* Use extrapolated logic and fix omission in helper

* Make freezing actually happen and document usage

* In script, don't freeze PRs as long as they're preapproved to be edited by SOMEBODY

* Lint fixes
This commit is contained in:
Mike McNeil 2022-05-06 00:12:50 -05:00 committed by GitHub
parent c96f01b7f9
commit 5fe7fea24d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 229 additions and 101 deletions

View File

@ -7,6 +7,9 @@ module.exports = {
description: 'Receive webhook requests and/or incoming auth redirects from GitHub.',
extendedDescription: 'Useful for automation, visibility of changes, and abuse monitoring.',
inputs: {
botSignature: { type: 'string', },
action: { type: 'string', example: 'opened', defaultsTo: 'ping', moreInfoUrl: 'https://developer.github.com/v3/activity/events/types' },
@ -26,7 +29,6 @@ module.exports = {
let IS_FROZEN = true;// « Set this to `true` whenever a freeze is in effect, then set it back to `false` when the freeze ends.
let GREEN_LABEL_COLOR = 'C2E0C6';// « Used in multiple places below.
let GITHUB_USERNAMES_OF_BOTS_AND_MAINTAINERS = [// « Used in multiple places below.
'sailsbot',
'fleet-release',
@ -60,7 +62,10 @@ module.exports = {
'juan-fdz-hawa',
'roperzh',
];
let GITHUB_USERNAME_OF_DRI_FOR_LABELS = 'noahtalerman';// « Used below
let GREEN_LABEL_COLOR = 'C2E0C6';// « Used in multiple places below. (FUTURE: Use the "+" prefix for this instead of color. 2022-05-05)
let GITHUB_USERNAME_OF_DRI_FOR_LABELS = 'noahtalerman';// « Used below (FUTURE: Remove this capability as Fleet has outgrown it. 2022-05-05)
if (!sails.config.custom.slackWebhookUrlForGithubBot) {
throw new Error('No Slack webhook URL configured for the GitHub bot to notify with alerts! (Please set `sails.config.custom.slackWebhookUrlForGithubBot`.)');
@ -200,76 +205,13 @@ module.exports = {
'Authorization': `token ${sails.config.custom.githubAccessToken}`
};
require('assert')(sender.login !== undefined);
// Check whether auto-approval is warranted.
let isAutoApproved = await sails.helpers.flow.build(async()=>{
let isSenderDRIForAllChangedPaths = false;
let isSenderMaintainer = GITHUB_USERNAMES_OF_BOTS_AND_MAINTAINERS.includes(sender.login.toLowerCase());
let DRI_BY_PATH = {
'README.md': 'mike-j-thomas',// (github brandfront)
'handbook': ['desmi-dizney', 'mike-j-thomas', 'mikermcneil'],// (default for handbook)
'handbook/company.md': 'mikermcneil',
'handbook/people.md': ['eashaw', 'mike-j-thomas'],
'handbook/engineering.md': 'zwass',
'handbook/product.md': 'noahtalerman',
'handbook/security.md': 'guillaumeross',
'handbook/security-policies.md': 'guillaumeross',
'handbook/brand.md': 'mike-j-thomas',
'handbook/growth.md': 'timmy-k',
'handbook/customers.md': 'tgauda',
'handbook/community.md': ['dominuskelvin', 'ksatter'],
'handbook/README.md': '*',// (any fleetie can update this page)
'website': 'mikermcneil',// (default for website)
'website/views': 'eashaw',
'website/assets': 'eashaw',
'website/config/routes.js': ['eashaw', 'mike-j-thomas'],// (for managing website URLs)
'docs': 'zwass',// (default for docs)
'docs/images': ['noahtalerman', 'eashaw', 'mike-j-thomas'],
'docs/Using-Fleet/REST-API.md': 'lukeheath',
'docs/Contributing/API-for-contributors.md': 'lukeheath',
'docs/Deploying/FAQ.md': ['ksatter', 'dominuskelvin'],
'docs/Contributing/FAQ.md': ['ksatter', 'dominuskelvin'],
'docs/Using-Fleet/FAQ.md': ['ksatter', 'dominuskelvin'],
'docs/01-Using-Fleet/standard-query-library/standard-query-library.yml': 'guillaumeross',// (standard query library)
};
// [?] https://docs.github.com/en/rest/reference/pulls#list-pull-requests-files
let changedPaths = _.pluck(await sails.helpers.http.get(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files`, {
per_page: 100,//eslint-disable-line camelcase
}, baseHeaders).retry(), 'filename');// (don't worry, it's the whole path, not the filename)
sails.log.verbose(`Received notice that a new PR (#${prNumber}) was opened that changes the following paths:`, changedPaths);
isSenderDRIForAllChangedPaths = _.all(changedPaths, (changedPath)=>{
changedPath = changedPath.replace(/\/+$/,'');// « trim trailing slashes, just in case (b/c otherwise could loop forever)
require('assert')(sender.login !== undefined);
sails.log.verbose(`…checking DRI of changed path "${changedPath}"`);
let selfMergers = DRI_BY_PATH[changedPath] ? [].concat(DRI_BY_PATH[changedPath]) : [];// « ensure array
if (selfMergers.includes(sender.login.toLowerCase()) || (isSenderMaintainer && selfMergers.includes('*'))) {
return true;
}//•
let numRemainingPathsToCheck = changedPath.split('/').length;
while (numRemainingPathsToCheck > 0) {
let ancestralPath = changedPath.split('/').slice(0, -1 * numRemainingPathsToCheck).join('/');
sails.log.verbose(`…checking DRI of ancestral path "${ancestralPath}" for changed path`);
let selfMergers = DRI_BY_PATH[ancestralPath] ? [].concat(DRI_BY_PATH[ancestralPath]) : [];// « ensure array
if (selfMergers.includes(sender.login.toLowerCase()) || (isSenderMaintainer && selfMergers.includes('*'))) {
return true;
}//•
numRemainingPathsToCheck--;
}//∞
});//∞
if (isSenderDRIForAllChangedPaths && changedPaths.length < 100) {
return true;
} else {
return false;
}
let isAutoApproved = await sails.helpers.githubAutomations.getIsPrPreapproved.with({
prNumber: prNumber,
githubUserToCheck: sender.login,
isGithubUserMaintainerOrDoesntMatter: GITHUB_USERNAMES_OF_BOTS_AND_MAINTAINERS.includes(sender.login.toLowerCase())
});
// Now, if appropriate, auto-approve the change.
@ -282,38 +224,10 @@ module.exports = {
// [?] https://docs.github.com/en/rest/reference/pulls#create-a-review-for-a-pull-request
await sails.helpers.http.post(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`, {
event: 'REQUEST_CHANGES',
body: 'The repository is currently frozen for an upcoming release. Please do not merge this change yet. After the freeze has ended, please come back, edit your PR description, hit the spacebar a few times to make an arbitrary change, then save your PR. You will then be able to merge your change! In case of emergency, you can dismiss this review.'
body: 'The repository is currently frozen for an upcoming release. \n> After the freeze has ended, please dismiss this review. \n\nIn case of emergency, you can dismiss this review and merge now.'
}, baseHeaders);
}
// And also unfreeze and re-freeze to temporarily allow merging.
// [?] https://github.com/fleetdm/fleet/issues/5179
// // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// > Temporarily disable re-freeze logic to troubleshoot website caching issues on 2022-05-03.
// > (very unlikely this is a culprit, but ruling out race conditions)
// > ~mikermcneil
// // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// if (isAutoApproved) {
//
// let mergeFreezeReport = await sails.helpers.http.get('https://www.mergefreeze.com/api/branches/fleetdm/fleet/main', { access_token: sails.config.custom.mergeFreezeAccessToken });//eslint-disable-line camelcase
// if (mergeFreezeReport.frozen) {
// await sails.helpers.http.post('https://www.mergefreeze.com/api/branches/fleetdm/fleet/main', { frozen: false, access_token: sails.config.custom.mergeFreezeAccessToken, user_name: 'fleet-release' });//eslint-disable-line camelcase
//
// // Then, in the background, 2 minutes later...
// setTimeout(()=>{
// sails.helpers.http.post('https://www.mergefreeze.com/api/branches/fleetdm/fleet/main', { frozen: true, access_token: sails.config.custom.mergeFreezeAccessToken, user_name: 'fleet-release' })//eslint-disable-line camelcase
// .exec((err)=>{
// if (err) {
// sails.log.error('Background instruction failed: Unexpected error re-freezing repo (see https://github.com/fleetdm/fleet/issues/5179 for background):', err);
// }
// sails.log.info('Re-freeze completed successfully.');
// });//_∏_
// }, 2*60*1000);//_∏_
// }//fi
//
// }//fi
// // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
}
} else if (ghNoun === 'issue_comment' && ['created'].includes(action) && (issueOrPr&&issueOrPr.state === 'open')) {
// ██████╗ ██████╗ ███╗ ███╗███╗ ███╗███████╗███╗ ██╗████████╗

View File

@ -0,0 +1,89 @@
module.exports = {
friendlyName: 'Get "is PR preapproved?"',
description: '',
inputs: {
prNumber: { type: 'number', example: 382, required: true },
githubUserToCheck: { type: 'string', example: 'mikermcneil', description: 'If excluded, then this returns `true` if all of the PRs changed paths are preapproved for SOMEONE.' },
isGithubUserMaintainerOrDoesntMatter: { type: 'boolean', required: true, description: 'Whether (a) the user is a maintainer, or (b) it even matters for this check whether the user is a maintainer.' },// FUTURE: « this could be replaced with an extra GitHub API call herein, but doesn't seem worth it
},
exits: {
success: {
outputFriendlyName: 'Is PR preapproved?',
outputDescription: 'Whether the provided GitHub user is the DRI for all changed paths.',
outputType: 'boolean',
},
},
fn: async function ({prNumber, githubUserToCheck, isGithubUserMaintainerOrDoesntMatter}) {
require('assert')(sails.config.custom.githubRepoDRIByPath);
require('assert')(sails.config.custom.githubAccessToken);
let DRI_BY_PATH = sails.config.custom.githubRepoDRIByPath;
let owner = 'fleetdm';
let repo = 'fleet';
let baseHeaders = {
'User-Agent': 'sails run freeze-open-pull-requests',
'Authorization': `token ${sails.config.custom.githubAccessToken}`
};
// Check the PR's author versus the intersection of DRIs for all changed files.
return await sails.helpers.flow.build(async()=>{
let isDRIForAllChangedPathsStill = false;
// [?] https://docs.github.com/en/rest/reference/pulls#list-pull-requests-files
let changedPaths = _.pluck(await sails.helpers.http.get(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files`, {
per_page: 100,//eslint-disable-line camelcase
}, baseHeaders).retry(), 'filename');// (don't worry, it's the whole path, not the filename)
isDRIForAllChangedPathsStill = _.all(changedPaths, (changedPath)=>{
changedPath = changedPath.replace(/\/+$/,'');// « trim trailing slashes, just in case (b/c otherwise could loop forever)
// sails.log.verbose(`…checking DRI of changed path "${changedPath}"`);
let selfMergers = DRI_BY_PATH[changedPath] ? [].concat(DRI_BY_PATH[changedPath]) : [];// « ensure array
if (!githubUserToCheck && selfMergers.length >= 1) {// « not checking a user, so just make sure all these paths are preapproved for SOMEONE
return true;
}
if (githubUserToCheck && (selfMergers.includes(githubUserToCheck.toLowerCase()) || (isGithubUserMaintainerOrDoesntMatter && selfMergers.includes('*')))) {
return true;
}//•
let numRemainingPathsToCheck = changedPath.split('/').length;
while (numRemainingPathsToCheck > 0) {
let ancestralPath = changedPath.split('/').slice(0, -1 * numRemainingPathsToCheck).join('/');
// sails.log.verbose(`…checking DRI of ancestral path "${ancestralPath}" for changed path`);
let selfMergers = DRI_BY_PATH[ancestralPath] ? [].concat(DRI_BY_PATH[ancestralPath]) : [];// « ensure array
if (!githubUserToCheck && selfMergers.length >= 1) {// « not checking a user, so just make sure all these paths are preapproved for SOMEONE
return true;
}
if (githubUserToCheck && (selfMergers.includes(githubUserToCheck.toLowerCase()) || (isGithubUserMaintainerOrDoesntMatter && selfMergers.includes('*')))) {
return true;
}//•
numRemainingPathsToCheck--;
}//∞
});//∞
if (isDRIForAllChangedPathsStill && changedPaths.length < 100) {
return true;
} else {
return false;
}
});
}
};

View File

@ -14,7 +14,7 @@ module.exports.custom = {
* *
* The base URL to use during development. *
* *
*  No trailing slash at the end *
* No trailing slash at the end *
* `http://` or `https://` at the beginning. *
* *
* > This is for use in custom logic that builds URLs. *
@ -85,6 +85,48 @@ module.exports.custom = {
// || (Or if you don't need billing, feel free to remove them.)
//--------------------------------------------------------------------------
/***************************************************************************
* *
* Directly responsible individuals (DRIs) whose changes to areas of the *
* code respository (outside of the core product code) are auto-approved, *
* even during code freezes. *
* *
* See api/controllers/webhooks/receive-from-github.js for context. *
* *
***************************************************************************/
githubRepoDRIByPath: {
'README.md': 'mike-j-thomas',// (github brandfront)
'handbook': ['desmi-dizney', 'mike-j-thomas', 'mikermcneil'],// (default for handbook)
'handbook/company.md': 'mikermcneil',
'handbook/people.md': ['eashaw', 'mike-j-thomas'],
'handbook/engineering.md': 'zwass',
'handbook/product.md': 'noahtalerman',
'handbook/security.md': 'guillaumeross',
'handbook/security-policies.md': 'guillaumeross',
'handbook/brand.md': 'mike-j-thomas',
'handbook/growth.md': 'timmy-k',
'handbook/customers.md': 'tgauda',
'handbook/community.md': ['dominuskelvin', 'ksatter'],
'handbook/README.md': '*',// (any fleetie can update this page and merge their change without waiting for their change to be approved)
'website': 'mikermcneil',// (default for website)
'website/views': 'eashaw',
'website/assets': 'eashaw',
'website/config/routes.js': ['eashaw', 'mike-j-thomas'],// (for managing website URLs)
'docs': 'zwass',// (default for docs)
'docs/images': ['noahtalerman', 'eashaw', 'mike-j-thomas'],
'docs/Using-Fleet/REST-API.md': 'lukeheath',
'docs/Contributing/API-for-contributors.md': 'lukeheath',
'docs/Deploying/FAQ.md': ['ksatter', 'dominuskelvin'],
'docs/Contributing/FAQ.md': ['ksatter', 'dominuskelvin'],
'docs/Using-Fleet/FAQ.md': ['ksatter', 'dominuskelvin'],
'docs/01-Using-Fleet/standard-query-library/standard-query-library.yml': 'guillaumeross',// (standard query library)
},
/***************************************************************************
* *
* Any other custom config this Sails app should use during development. *

View File

@ -0,0 +1,83 @@
module.exports = {
friendlyName: 'Freeze open pull requests',
description: 'Freeze existing pull requests open on https://github.com/fleetdm/fleet, except those that consist exclusively of changes to files where the author is the DRI, according to auto-approval rules.',
extendedDescription: `# Usage
## Dry run
sails_custom__githubAccessToken=YOUR_TOKEN_HERE sails run scripts/freeze-open-pull-requests.js --dry
## The real deal
sails_custom__githubAccessToken=YOUR_TOKEN_HERE sails run scripts/freeze-open-pull-requests.js --limit=100`,
inputs: {
dry: { type: 'boolean', defaultsTo: false, description: 'Whether to do a dry run, and not actually freeze anything.' },
limit: { type: 'number', defaultsTo: 100, description: 'The max number of PRs to examine and potentially freeze. (Useful for testing.)' },
},
fn: async function ({dry: isDryRun, limit: maxNumPullRequestsToCheck }) {
sails.log('Running custom shell script... (`sails run freeze-open-pull-requests`)');
let owner = 'fleetdm';
let repo = 'fleet';
let baseHeaders = {
'User-Agent': 'sails run freeze-open-pull-requests',
'Authorization': `token ${sails.config.custom.githubAccessToken}`
};
// Fetch open pull requests
// [?] https://docs.github.com/en/rest/pulls/pulls#list-pull-requests
let openPullRequests = await sails.helpers.http.get(`https://api.github.com/repos/${owner}/${repo}/pulls`, {
state: 'open',
per_page: maxNumPullRequestsToCheck,//eslint-disable-line camelcase
}, baseHeaders);
if (openPullRequests.length > maxNumPullRequestsToCheck) {
openPullRequests = openPullRequests.slice(0,maxNumPullRequestsToCheck);
}
let SECONDS_TO_WAIT = 5;
sails.log(`Examining and potentially freezing ${openPullRequests.length} PRs very soon… (To cancel, press CTRL+C quickly within ${SECONDS_TO_WAIT}s!)`);
await sails.helpers.flow.pause(SECONDS_TO_WAIT*1000);
// For all open pull requests…
await sails.helpers.flow.simultaneouslyForEach(openPullRequests, async(pullRequest)=>{
let prNumber = pullRequest.number;
let prAuthor = pullRequest.user.login;
require('assert')(prAuthor !== undefined);
// Freeze, if appropriate.
// (Check versus the intersection of DRIs for all changed files to make sure SOMEONE is preapproved for all of them.)
let isAuthorPreapproved = await sails.helpers.githubAutomations.getIsPrPreapproved.with({
prNumber: prNumber,
isGithubUserMaintainerOrDoesntMatter: true// « doesn't matter here because no auto-approval is happening. Worst case, a community PR to an area with a "*" in the DRI mapping remains unfrozen.
});
if (isDryRun) {
sails.log(`#${prNumber} by @${prAuthor}:`, isAuthorPreapproved ? 'Would have skipped freeze…' : 'Would have frozen…');
} else {
sails.log(`#${prNumber} by @${prAuthor}:`, isAuthorPreapproved ? 'Skipping freeze…' : 'Freezing…');
if (!isAuthorPreapproved) {
// [?] https://docs.github.com/en/rest/reference/pulls#create-a-review-for-a-pull-request
await sails.helpers.http.post(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`, {
event: 'REQUEST_CHANGES',
body: 'The repository has been frozen for an upcoming release. In case of emergency, you can dismiss this review and merge.'
}, baseHeaders);
}//fi
}
});
}
};