[pull] master from commaai:master #8
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
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/[email protected] | |
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"); | |
}); | |
} | |