mirror of https://github.com/commaai/openpilot.git
208 lines
8.3 KiB
YAML
208 lines
8.3 KiB
YAML
name: "PR review"
|
|
on:
|
|
pull_request_target:
|
|
types: [opened, reopened, synchronize, edited, edited]
|
|
|
|
jobs:
|
|
labeler:
|
|
name: apply labels
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
submodules: false
|
|
- uses: actions/labeler@v5.0.0
|
|
with:
|
|
dot: true
|
|
configuration-path: .github/labeler.yaml
|
|
|
|
pr_branch_check:
|
|
name: check branch
|
|
runs-on: ubuntu-latest
|
|
if: github.repository == 'commaai/openpilot'
|
|
steps:
|
|
- uses: Vankka/pr-target-branch-action@69ab6dd5c221de3548b3b6c4d102c1f4913d3baa
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
with:
|
|
target: /^(?!master$).*/
|
|
exclude: /commaai:.*/
|
|
change-to: ${{ github.base_ref }}
|
|
already-exists-action: close_this
|
|
already-exists-comment: "Your PR should be made against the `master` branch"
|
|
|
|
check-pr-template:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
issues: write
|
|
pull-requests: write
|
|
actions: read
|
|
if: github.event.pull_request.head.repo.full_name != 'commaai/openpilot'
|
|
steps:
|
|
- uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
// Comment to add to the PR if no template has been used
|
|
const NO_TEMPLATE_MESSAGE =
|
|
"It looks like you didn't use one of the Pull Request templates. Please check [the contributing docs](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md). \
|
|
Also make sure that you didn't modify any of the checkboxes or headings within the template.";
|
|
// body data for future requests
|
|
const body_data = {
|
|
issue_number: context.issue.number,
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
};
|
|
|
|
// Utility function to extract all headings
|
|
const extractHeadings = (markdown) => {
|
|
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
|
|
const boldTextRegex = /^(?:\*\*|__)(.+?)(?:\*\*|__)\s*$/gm;
|
|
const headings = [];
|
|
let headingMatch;
|
|
while ((headingMatch = headingRegex.exec(markdown))) {
|
|
headings.push(headingMatch[2].trim());
|
|
}
|
|
let boldMatch;
|
|
while ((boldMatch = boldTextRegex.exec(markdown))) {
|
|
headings.push(boldMatch[1].trim());
|
|
}
|
|
return headings;
|
|
};
|
|
|
|
// Utility function to extract all check box descriptions
|
|
const extractCheckBoxTexts = (markdown) => {
|
|
const checkboxRegex = /^\s*-\s*\[( |x)\]\s+(.+)$/gm;
|
|
const checkboxes = [];
|
|
let match;
|
|
while ((match = checkboxRegex.exec(markdown))) {
|
|
checkboxes.push(match[2].trim());
|
|
}
|
|
return checkboxes;
|
|
};
|
|
|
|
// Utility function to check if a list is a subset of another list
|
|
isSubset = (subset, superset) => {
|
|
return subset.every((item) => superset.includes(item));
|
|
};
|
|
|
|
// Utility function to check if a list of checkboxes is a subset of another list of checkboxes
|
|
isCheckboxSubset = (templateCheckBoxTexts, prTextCheckBoxTexts) => {
|
|
// Check if each template checkbox text is a substring of at least one PR checkbox text
|
|
// (user should be allowed to add additional text)
|
|
return templateCheckBoxTexts.every((item) => prTextCheckBoxTexts.some((element) => element.includes(item)))
|
|
}
|
|
|
|
// Get filenames of all currently checked-in PR templates
|
|
const template_contents = await github.rest.repos.getContent({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
path: ".github/PULL_REQUEST_TEMPLATE",
|
|
});
|
|
var template_filenames = [];
|
|
for (const content of template_contents.data) {
|
|
template_filenames.push(content.path);
|
|
}
|
|
console.debug("Received template filenames: " + template_filenames);
|
|
// Retrieve templates
|
|
var templates = [];
|
|
for (const template_filename of template_filenames) {
|
|
const template_response = await github.rest.repos.getContent({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
path: template_filename,
|
|
});
|
|
// Convert Base64 content back
|
|
const decoded_template = atob(template_response.data.content);
|
|
const headings = extractHeadings(decoded_template);
|
|
const checkboxes = extractCheckBoxTexts(decoded_template);
|
|
if (!headings.length && !checkboxes.length) {
|
|
console.warn(
|
|
"Invalid template! Contains neither headings nor checkboxes, ignoring it: \n" +
|
|
decoded_template
|
|
);
|
|
} else {
|
|
templates.push({ headings: headings, checkboxes: checkboxes });
|
|
}
|
|
}
|
|
// Retrieve the PR Body
|
|
const pull_request = await github.rest.issues.get({
|
|
...body_data,
|
|
});
|
|
const pull_request_text = pull_request.data.body;
|
|
console.debug("Received Pull Request body: \n" + pull_request_text);
|
|
|
|
/* Check if the PR Body matches one of the templates
|
|
A template is defined by all headings and checkboxes it contains
|
|
We extract all Headings and Checkboxes from the PR text and check if any of the templates is a subset of that
|
|
*/
|
|
const pr_headings = extractHeadings(pull_request_text);
|
|
const pr_checkboxes = extractCheckBoxTexts(pull_request_text);
|
|
console.debug("Found Headings in PR body:\n" + pr_headings);
|
|
console.debug("Found Checkboxes in PR body:\n" + pr_checkboxes);
|
|
var template_found = false;
|
|
// Iterate over each template to check if it applies
|
|
for (const template of templates) {
|
|
console.log(
|
|
"Checking for headings: [" +
|
|
template.headings +
|
|
"] and checkboxes: [" +
|
|
template.checkboxes + "]"
|
|
);
|
|
if (
|
|
isCheckboxSubset(template.checkboxes, pr_checkboxes) &&
|
|
isSubset(template.headings, pr_headings)
|
|
) {
|
|
console.debug("Found matching template!");
|
|
template_found = true;
|
|
}
|
|
}
|
|
|
|
// List comments from previous runs
|
|
var existing_comments = [];
|
|
const comments = await github.rest.issues.listComments({
|
|
...body_data,
|
|
});
|
|
for (const comment of comments.data) {
|
|
if (comment.body === NO_TEMPLATE_MESSAGE) {
|
|
existing_comments.push(comment);
|
|
}
|
|
}
|
|
|
|
// Add a comment to the PR that it is not using a the template (but only if this comment does not exist already)
|
|
if (!template_found) {
|
|
var comment_already_sent = false;
|
|
|
|
// Add an 'in-bot-review' label since this PR doesn't have the template
|
|
github.rest.issues.addLabels({
|
|
...body_data,
|
|
labels: ["in-bot-review"],
|
|
});
|
|
|
|
if (existing_comments.length < 1) {
|
|
github.rest.issues.createComment({
|
|
...body_data,
|
|
body: NO_TEMPLATE_MESSAGE,
|
|
});
|
|
}
|
|
} else {
|
|
// If template has been found, delete any old comment about missing template
|
|
for (const existing_comment of existing_comments) {
|
|
github.rest.issues.deleteComment({
|
|
...body_data,
|
|
comment_id: existing_comment.id,
|
|
});
|
|
}
|
|
// Remove the 'in-bot-review' label after the review is done and the PR has passed
|
|
github.rest.issues.removeLabel({
|
|
...body_data,
|
|
name: "in-bot-review",
|
|
}).catch((error) => {
|
|
console.log("Label 'in-bot-review' not found, ignoring");
|
|
});
|
|
}
|
|
|