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 and Title Length
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 if (title.length >= 40) {
33 errors.push(`- **Title**: Is too long (${title.length} characters). The PR title must be strictly under 40 characters.`);
34 }
35
36 // 2. Check PR Template adherence
37 if (!body.includes("## What?") || !body.includes("## Why?")) {
38 errors.push("- **Body**: Missing the `## What?` or `## Why?` headings required by the PR template.");
39 }
40
41 // Check if they just left the HTML comments from the template empty
42 const commentRegex = new RegExp("", "g");
43 const cleanedBody = body.replace(commentRegex, "").trim();
44 if (cleanedBody.length < 10) {
45 errors.push("- **Body**: The PR description is too short or hasn't replaced the template placeholders.");
46 }
47
48 // 3. Request Changes or Dismiss previous requests
49 if (errors.length > 0) {
50 const message = `Hi @${context.actor}! Please fix the following issues with your PR:\n\n${errors.join("\n")}`;
51
52 await github.rest.pulls.createReview({
53 owner,
54 repo,
55 pull_number,
56 body: message,
57 event: 'REQUEST_CHANGES'
58 });
59
60 core.setFailed("PR formatting checks failed.");
61 } else {
62 // The PR is now valid. Let's find out who the bot is to dismiss its own reviews.
63 const { data: botUser } = await github.rest.users.getAuthenticated();
64
65 // Fetch all reviews on this PR
66 const { data: reviews } = await github.rest.pulls.listReviews({
67 owner,
68 repo,
69 pull_number
70 });
71
72 // Find active blocking reviews left by the bot account
73 const botReviews = reviews.filter(r =>
74 r.user.login === botUser.login &&
75 r.state === 'CHANGES_REQUESTED'
76 );
77
78 // Dismiss them so the PR is unblocked
79 for (const review of botReviews) {
80 await github.rest.pulls.dismissReview({
81 owner,
82 repo,
83 pull_number,
84 review_id: review.id,
85 message: 'Formatting issues have been resolved. Thank you!'
86 });
87 }
88 }