Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic checklist POC #3

Closed
wants to merge 60 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
3349d1c
Add skeleton of dynamicChecklist action
roryabraham Aug 31, 2023
b4c0d80
Generate dynamic checklist
roryabraham Aug 31, 2023
d79295b
Add new action to build script
roryabraham Aug 31, 2023
c3c0759
Add dynamicChecklist workflow
roryabraham Aug 31, 2023
f8f16a9
Add notes about checking results as we go
roryabraham Aug 31, 2023
b4a8c42
Add checklist notes
roryabraham Aug 31, 2023
6e8f83e
Add isPassed flag and failing pipeline if falsey
sebryu Sep 13, 2023
47e6817
Changed action path in workflow for testing
sebryu Sep 13, 2023
b23966c
Testing GH action
sebryu Sep 13, 2023
11823b5
Fixed checks categories logic
sebryu Sep 13, 2023
03a601f
Fixed regex capture group
sebryu Sep 15, 2023
62fd8a4
Added missing else clause
sebryu Sep 15, 2023
a7c94db
Added core import into custom action
sebryu Sep 15, 2023
b0c8fcb
Merged checklist actions into single action
sebryu Sep 15, 2023
e4a5f36
Reverted testing changes
sebryu Sep 15, 2023
8f09990
Added logic to remove dynamic check from PR description if it's not r…
sebryu Sep 15, 2023
51570ba
Merge branch 'main' into Sebryu-DynamicChecklistPOC
sebryu Sep 22, 2023
d6d5ced
Added newComponentCategory to authorChecklist, with detection of reac…
sebryu Sep 22, 2023
53b4ffd
Merge branch 'main' into Sebryu-DynamicChecklistPOC
sebryu Sep 25, 2023
db354cf
Test commit - testing GH action from current branch - revert later
sebryu Sep 25, 2023
198bda2
Fixed undefined file name
sebryu Sep 25, 2023
b35b6b9
Test commit - testing GH action on separate repo
sebryu Sep 25, 2023
162c07b
Fixed changedFiles filename reference
sebryu Sep 25, 2023
d80c9b0
Reading whole file for detecting react component
sebryu Sep 25, 2023
c2136a6
Using relative path to files in detectFunction
sebryu Sep 25, 2023
d7b0c73
Using absolute path in detectFunction
sebryu Sep 25, 2023
99e431e
Fixed path in detectFunction
sebryu Sep 25, 2023
f3f5910
Fixing read file
sebryu Sep 25, 2023
7bd1e44
Debugging read files problems
sebryu Sep 25, 2023
c8731df
Debugging read files problems
sebryu Sep 25, 2023
624148c
Debugging read files problems
sebryu Sep 25, 2023
92ef0f4
Fetching files for detecting react component in GH action
sebryu Sep 26, 2023
90708ea
Fixed octokit usage
sebryu Sep 26, 2023
20aaac4
Detecting react component and fetching files in promise.race
sebryu Sep 26, 2023
5508814
Debugging GH action
sebryu Sep 26, 2023
02c65f5
Decoding base64 files from GH action
sebryu Sep 26, 2023
dbdcafb
Rewritted Promise race to regular for loop
sebryu Sep 26, 2023
ba51334
Detecting react component - log results
sebryu Sep 26, 2023
3b339d1
Added synchronize event so PR Author Checklist workflow will run on n…
sebryu Sep 26, 2023
8dcae63
Searching for react component only in added files
sebryu Sep 26, 2023
1697bb3
Testing GH action
sebryu Sep 26, 2023
92ef235
Rewrote _.each to regular for loop for better async handling
sebryu Sep 26, 2023
fe69583
Fixed iteration over object
sebryu Sep 26, 2023
5dc3ce4
Escaping checklist items for regexp
sebryu Sep 26, 2023
aedc1e8
Fixed calling set.length instead of set.size
sebryu Sep 26, 2023
b2e0857
Removed test file
sebryu Sep 26, 2023
445e118
Removed return statement from for loop
sebryu Sep 26, 2023
9e6194f
Fixed getting all checks
sebryu Sep 26, 2023
974f1f7
Detecting when PR description has been changed
sebryu Sep 26, 2023
584b03c
Prettier auto format
sebryu Sep 26, 2023
fb0c821
Dummy file to test workflow
sebryu Sep 26, 2023
6872dcc
Better marker for ending of checklist section
sebryu Sep 26, 2023
8f54c12
More debug for GH description editing
sebryu Sep 26, 2023
36e40eb
Debugging GH action
sebryu Sep 26, 2023
a0744e6
Debugging GH action
sebryu Sep 26, 2023
7c43f1e
Fixed splitting string with new line character
sebryu Sep 26, 2023
2db2b14
Removed dummy file
sebryu Sep 26, 2023
9149b48
Updated caret return char in other regexps
sebryu Sep 26, 2023
e82348f
Reverted const to original repo
sebryu Sep 26, 2023
ec1a671
Reverted workflow action path to main branch
sebryu Sep 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 2 additions & 14 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ $ https://github.com/Expensify/App/issues/<issueID(comment)>

Do NOT only link the issue number like this: $ #<issueID>
--->
$
PROPOSAL:
$
PROPOSAL:


### Tests
Expand Down Expand Up @@ -94,17 +94,6 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c
- [ ] I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
- [ ] I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such
- [ ] I verified that if a function's arguments changed that all usages have also been updated correctly
- [ ] If a new component is created I verified that:
- [ ] A similar component doesn't exist in the codebase
- [ ] All props are defined accurately and each prop has a `/** comment above it */`
- [ ] The file is named correctly
- [ ] The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
- [ ] The only data being stored in the state is data necessary for rendering and nothing else
- [ ] If we are not using the full Onyx data that we loaded, I've added the proper selector in order to ensure the component only re-renders when the data it is using changes
- [ ] For Class Components, any internal methods passed to components event handlers are bound to `this` properly so there are no scoping issues (i.e. for `onClick={this.submit}` the method `this.submit` should be bound to `this` in the constructor)
- [ ] Any internal methods bound to `this` are necessary to be bound (i.e. avoid `this.submit = this.submit.bind(this);` if `this.submit` is never passed to a component event handler like `onClick`)
- [ ] All JSX used for rendering exists in the render method
- [ ] The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
- [ ] If any new file was added I verified that:
- [ ] The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
- [ ] If a new CSS style is added I verified that:
Expand All @@ -116,7 +105,6 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c
- [ ] If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
- [ ] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page.
- [ ] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps.
- [ ] I have checked off every checkbox in the PR author checklist, including those that don't apply to this PR.

### Screenshots/Videos
<details>
Expand Down
174 changes: 145 additions & 29 deletions .github/actions/javascript/authorChecklist/authorChecklist.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,56 @@
const core = require('@actions/core');
const github = require('@actions/github');
const https = require('https');
const GitHubUtils = require('../../../libs/GithubUtils');
const _ = require('lodash');
const GithubUtils = require('../../../libs/GithubUtils');
const CONST = require('../../../libs/CONST');
const newComponentCategory = require('./newComponentCategory');

const pathToAuthorChecklist = 'https://raw.githubusercontent.com/Expensify/App/main/.github/PULL_REQUEST_TEMPLATE.md';
const authorChecklistStartsWith = '### PR Author Checklist';
const reviewerChecklistStartsWith = '<summary><h4>PR Reviewer Checklist</h4>';
const issue = github.context.payload.issue ? github.context.payload.issue.number : github.context.payload.pull_request.number;
const checklistStartsWith = '### PR Author Checklist';
const checklistEndsWith = "\r\n### Screenshots/Videos";

const prNumber = github.context.payload.pull_request.number;

const CHECKLIST_CATEGORIES = {
NEW_COMPONENT: newComponentCategory,
};

/**
* Look at the contents of the pull request, and determine which checklist categories apply.
*
* @returns {Promise<Array<String>>}
*/
async function getChecklistCategoriesForPullRequest() {
const categories = [];
const changedFiles = await GithubUtils.paginate(GithubUtils.octokit.pulls.listFiles, {
owner: CONST.GITHUB_OWNER,
repo: CONST.APP_REPO,
pull_number: prNumber,
per_page: 100,
});

for (const category of _.values(CHECKLIST_CATEGORIES)) {
const {detectFunction, items} = category;
const categoryDetected = await detectFunction(changedFiles);
if (categoryDetected) {
categories.push(items);
}
}
return categories;
}

/**
* Takes string in markdown, and divides it's content by two contant markers.
*
* @param {String} body
* @returns {[String, String, String]}
*/
function partitionWithChecklist(body) {
const [contentBeforeChecklist, contentAfterStartOfChecklist] = body.split(checklistStartsWith);
const [checklistContent, contentAfterChecklist] = contentAfterStartOfChecklist.split(checklistEndsWith);
return [contentBeforeChecklist, checklistContent, contentAfterChecklist];
}

/**
* @returns {Promise}
Expand All @@ -20,12 +64,10 @@ function getNumberOfItemsFromAuthorChecklist() {
fileContents += chunk;
});
res.on('end', () => {
// Currently, both the author and reviewer checklists are in the PR template file, so we need to do a little bit of parsing the PR description to get just the author
// checklist.
const contentAfterStartOfAuthorChecklist = fileContents.split(authorChecklistStartsWith).pop();
const contentBeforeStartOfReviewerChecklist = contentAfterStartOfAuthorChecklist.split(reviewerChecklistStartsWith).shift();
// eslint-disable-next-line no-unused-vars
const [_start, checklist] = partitionWithChecklist(fileContents);

const numberOfChecklistItems = (contentBeforeStartOfReviewerChecklist.match(/\[ \]/g) || []).length;
const numberOfChecklistItems = (checklist.match(/\[ \]/g) || []).length;
resolve(numberOfChecklistItems);
});
})
Expand All @@ -35,33 +77,107 @@ function getNumberOfItemsFromAuthorChecklist() {

/**
* @param {Number} numberOfChecklistItems
* @param {String} pullRequestBody
*/
function checkIssueForCompletedChecklist(numberOfChecklistItems) {
GitHubUtils.getPullRequestBody(issue).then((pullRequestBody) => {
const contentAfterStartOfAuthorChecklist = pullRequestBody.split(authorChecklistStartsWith).pop();
const contentOfAuthorChecklist = contentAfterStartOfAuthorChecklist.split(reviewerChecklistStartsWith).shift();
function checkPRForCompletedChecklist(numberOfChecklistItems, pullRequestBody) {
// eslint-disable-next-line no-unused-vars
const [_start, checklist] = partitionWithChecklist(pullRequestBody);

const numberOfFinishedChecklistItems = (contentOfAuthorChecklist.match(/- \[x\]/gi) || []).length;
const numberOfUnfinishedChecklistItems = (contentOfAuthorChecklist.match(/- \[ \]/g) || []).length;
const numberOfFinishedChecklistItems = (checklist.match(/- \[x\]/gi) || []).length;
const numberOfUnfinishedChecklistItems = (checklist.match(/- \[ \]/g) || []).length;

const maxCompletedItems = numberOfChecklistItems + 2;
const minCompletedItems = numberOfChecklistItems - 2;
const minCompletedItems = numberOfChecklistItems - 2;

console.log(`You completed ${numberOfFinishedChecklistItems} out of ${numberOfChecklistItems} checklist items with ${numberOfUnfinishedChecklistItems} unfinished items`);
console.log(`You completed ${numberOfFinishedChecklistItems} out of ${numberOfChecklistItems} checklist items with ${numberOfUnfinishedChecklistItems} unfinished items`);

if (numberOfFinishedChecklistItems >= minCompletedItems && numberOfFinishedChecklistItems <= maxCompletedItems && numberOfUnfinishedChecklistItems === 0) {
console.log('PR Author checklist is complete 🎉');
return;
}
if (numberOfFinishedChecklistItems >= minCompletedItems && numberOfUnfinishedChecklistItems === 0) {
console.log('PR Author checklist is complete 🎉');
return;
}

console.log(`Make sure you are using the most up to date checklist found here: ${pathToAuthorChecklist}`);
core.setFailed("PR Author Checklist is not completely filled out. Please check every box to verify you've thought about the item.");
});
console.log(`Make sure you are using the most up to date checklist found here: ${pathToAuthorChecklist}`);
core.setFailed("PR Author Checklist is not completely filled out. Please check every box to verify you've thought about the item.");
}

getNumberOfItemsFromAuthorChecklist()
.then(checkIssueForCompletedChecklist)
.catch((err) => {
async function generateDynamicChecksAndCheckForCompletion() {
// Generate dynamic checks
const checks = new Set();
const categories = await getChecklistCategoriesForPullRequest();
for (const checksForCategory of categories) {
for (const check of checksForCategory) {
checks.add(check);
}
}

const body = github.context.payload.pull_request.body;

// eslint-disable-next-line prefer-const
let [contentBeforeChecklist, checklist, contentAfterChecklist] = partitionWithChecklist(body);

let isPassing = true;
let checklistChanged = false;
for (const check of checks) {
// Check if it's already in the PR body, capturing the whether or not it's already checked
const regex = new RegExp(`- \\[([ x])] ${_.escapeRegExp(check)}`);
const match = regex.exec(checklist);
if (!match) {
// Add it to the PR body
isPassing = false;
checklist += `- [ ] ${check}\r\n`;
checklistChanged = true;
} else {
const isChecked = match[1] === 'x';
if (!isChecked) {
isPassing = false;
}
}
}
const allChecks = _.flatten(_.map(_.values(CHECKLIST_CATEGORIES), 'items'));
for (const check of allChecks) {
if (!checks.has(check)) {
// Check if some dynamic check has been added with previous commit, but the check is not relevant anymore
const regex = new RegExp(`- \\[([ x])] ${_.escapeRegExp(check)}\r\n`);
const match = regex.exec(checklist);
if (match) {
// Remove it from the PR body
checklist = checklist.replace(match[0], '');
checklistChanged = true;
}
}
}

// Put the PR body back together, need to add the markers back in
const newBody = contentBeforeChecklist + checklistStartsWith + checklist + checklistEndsWith + contentAfterChecklist;

// Update the PR body
if (checklistChanged) {
await GithubUtils.octokit.pulls.update({
owner: CONST.GITHUB_OWNER,
repo: CONST.APP_REPO,
pull_number: prNumber,
body: newBody,
});
console.log('Updated PR checklist');
}

if (!isPassing) {
const err = new Error("New checks were added into checklist. Please check every box to verify you've thought about the item.");
console.error(err);
core.setFailed(err);
});
}

// check for completion
try {
const numberofItems = await getNumberOfItemsFromAuthorChecklist();
checkPRForCompletedChecklist(numberofItems, newBody);
} catch (err) {
console.error(err);
core.setFailed(err);
}
}

if (require.main === module) {
generateDynamicChecksAndCheckForCompletion();
}

module.exports = generateDynamicChecksAndCheckForCompletion;
Loading
Loading