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" comment: runs-on: ubuntu-latest steps: - name: comment uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 if: github.event.pull_request.head.repo.full_name != 'commaai/openpilot' with: message: | Thanks for contributing to openpilot! In order for us to review your PR as quickly as possible, check the following: * Convert your PR to a draft unless it's ready to review * Read the [contributing docs](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md) * Before marking as "ready for review", ensure: * the goal is clearly stated in the description * all the tests are passing * the change is [something we merge](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md#what-gets-merged) * include a route or your device' dongle ID if relevant comment_tag: run_id GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} check-pr-template: runs-on: ubuntu-latest permissions: contents: read issues: write pull-requests: write actions: read if: false && 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"); }); }