bot-pr-checks.yml

 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            }