1name: Bot - PR Formatting Checks
2
3on:
4 pull_request_target:
5 types: [opened, edited, synchronize]
6
7jobs:
8 check-pr:
9 runs-on: ubuntu-latest
10 steps:
11 - name: Validate PR Title and Body
12 uses: actions/github-script@v9
13 env:
14 PR_BODY: ${{ github.event.pull_request.body }}
15 PR_TITLE: ${{ github.event.pull_request.title }}
16 with:
17 github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
18 script: |
19 const title = process.env.PR_TITLE;
20 const body = process.env.PR_BODY || "";
21 const owner = context.repo.owner;
22 const repo = context.repo.repo;
23 const pull_number = context.issue.number;
24
25 let errors = [];
26
27 // 1. Check Conventional Commits
28 const ccRegex = /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9\-]+\))?:\s.+$/;
29 if (!ccRegex.test(title)) {
30 errors.push("- **Title**: Does not follow conventional commits (e.g., `feat: added something`, `fix(core): resolved crash`).");
31 }
32
33 // 2. Check PR Template adherence
34 if (!body.includes("## What?") || !body.includes("## Why?")) {
35 errors.push("- **Body**: Missing the `## What?` or `## Why?` headings required by the PR template.");
36 }
37
38 // Check if they just left the HTML comments from the template empty
39 const commentRegex = new RegExp("", "g");
40 const cleanedBody = body.replace(commentRegex, "").trim();
41
42 if (cleanedBody.length < 10) {
43 errors.push("- **Body**: The PR description is too short or hasn't replaced the template placeholders.");
44 }
45
46 // 3. Request Changes or Dismiss previous requests
47 if (errors.length > 0) {
48 const message = `Hi @${context.actor}! Please fix the following issues with your PR:\n\n${errors.join("\n")}`;
49
50 await github.rest.pulls.createReview({
51 owner,
52 repo,
53 pull_number,
54 body: message,
55 event: 'REQUEST_CHANGES'
56 });
57
58 core.setFailed("PR formatting checks failed.");
59 } else {
60 // The PR is now valid. Let's find out who the bot is to dismiss its own reviews.
61 const { data: botUser } = await github.rest.users.getAuthenticated();
62
63 // Fetch all reviews on this PR
64 const { data: reviews } = await github.rest.pulls.listReviews({
65 owner,
66 repo,
67 pull_number
68 });
69
70 // Find active blocking reviews left by the bot account
71 const botReviews = reviews.filter(r =>
72 r.user.login === botUser.login &&
73 r.state === 'CHANGES_REQUESTED'
74 );
75
76 // Dismiss them so the PR is unblocked
77 for (const review of botReviews) {
78 await github.rest.pulls.dismissReview({
79 owner,
80 repo,
81 pull_number,
82 review_id: review.id,
83 message: 'Formatting issues have been resolved. Thank you!'
84 });
85 }
86 }