Skip to content

Commit 90e3388

Browse files
committed
.github/workflows: Add PR formatting validator
This workflow runs when GitHub PRs are modified (edited, opened, reopened, and synchronized) to perform basic validation of the PR title and description. Right now, this includes: - Checking that PR template text does not remain in the PR description. - Checking that the PR body is not empty. If a check fails, a GitHub comment will be left on the PR and a PR status check failure will be present on the PR until the issue is resolved. Signed-off-by: Michael Kubacki <michael.kubacki@microsoft.com>
1 parent 2ff173a commit 90e3388

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# This workflow validates basic pull request formatting requirements are met.
2+
#
3+
# Copyright (c) Microsoft Corporation.
4+
# SPDX-License-Identifier: BSD-2-Clause-Patent
5+
#
6+
7+
name: Validate Pull Request Formatting
8+
9+
on:
10+
pull_request_target:
11+
branches:
12+
- master
13+
types:
14+
- edited
15+
- opened
16+
- reopened
17+
- synchronize
18+
19+
# Prevent concurrent runs for the same PR
20+
concurrency:
21+
group: pr-validation-${{ github.event.number }}
22+
cancel-in-progress: true
23+
24+
jobs:
25+
validate_pr:
26+
name: Validate Pull Request Formatting
27+
# Do not run on draft PRs and only run on PRs in the tianocore org
28+
if: ${{ github.event.pull_request.draft == false && github.repository_owner == 'tianocore' }}
29+
runs-on: ubuntu-latest
30+
31+
steps:
32+
- name: Generate Token
33+
id: app-token
34+
uses: actions/create-github-app-token@v2
35+
with:
36+
app-id: ${{ secrets.TIANOCORE_ASSIGN_REVIEWERS_APPLICATION_ID }}
37+
private-key: ${{ secrets.TIANOCORE_ASSIGN_REVIEWERS_APPLICATION_PRIVATE_KEY }}
38+
39+
- name: Validate PR Format
40+
id: validate
41+
run: |
42+
# Constants
43+
readonly VALIDATION_HEADER="Pull Request Validation Issues"
44+
45+
# Fetch PR data
46+
prData="$(gh api graphql -F owner=$OWNER -F name=$REPO -F pr_number=$PR_NUMBER -f query='
47+
query($name: String!, $owner: String!, $pr_number: Int!) {
48+
repository(owner: $owner, name: $name) {
49+
pullRequest(number: $pr_number) {
50+
title
51+
body
52+
}
53+
}
54+
}')"
55+
56+
prTitle=$(echo "$prData" | jq -r '.data.repository.pullRequest.title // ""')
57+
prBody=$(echo "$prData" | jq -r '.data.repository.pullRequest.body // ""')
58+
59+
# Fetch template content (may not exist)
60+
templateContent=""
61+
if gh api repos/$GITHUB_REPOSITORY/contents/.github/pull_request_template.md > /dev/null 2>&1; then
62+
templateContent="$(gh api repos/$GITHUB_REPOSITORY/contents/.github/pull_request_template.md --jq '.content' | base64 -d)"
63+
fi
64+
65+
validationError=false
66+
validationMessages=""
67+
68+
# Validate PR title
69+
if [[ -z "$prTitle" ]]; then
70+
validationMessages+="⚠️ Pull request title cannot be empty."$'\n'
71+
validationError=true
72+
fi
73+
74+
# Validate PR body
75+
if [[ -z "$prBody" || "$prBody" == "null" ]] || [[ -z "$(echo "$prBody" | xargs)" ]]; then
76+
validationMessages+="⚠️ Please add a meaningful pull request description using the PR template."$'\n'
77+
validationError=true
78+
# The minimum number of characters for a blank PR template is 147
79+
elif [[ ${#prBody} -lt 147 ]]; then
80+
validationMessages+="⚠️ Please provide a more detailed pull request description using the PR template (current: ${#prBody} characters)."$'\n'
81+
validationError=true
82+
fi
83+
84+
# Check for template lines if template exists
85+
if [[ -n "$templateContent" ]]; then
86+
# Template lines are considered to be those that begin with '<_' and end with '_>'
87+
templateLines=$(echo "$templateContent" | grep -E '^<_.*_>$' || true)
88+
89+
if [[ -n "$templateLines" ]]; then
90+
templateLinesFound=""
91+
while IFS= read -r line; do
92+
# Use more precise matching - check for the line as a standalone line
93+
if [[ -n "$line" ]] && echo "$prBody" | grep -Fxq "$line"; then
94+
if [[ -n "$templateLinesFound" ]]; then
95+
templateLinesFound="$templateLinesFound"$'\n'"\`$line\`"
96+
else
97+
templateLinesFound="\`$line\`"
98+
fi
99+
fi
100+
done <<< "$templateLines"
101+
102+
if [[ -n "$templateLinesFound" ]]; then
103+
validationMessages+="⚠️ Please remove the following template lines from your PR description:"$'\n'"$templateLinesFound"$'\n'
104+
validationError=true
105+
fi
106+
fi
107+
fi
108+
109+
# Build all validation messages in a single comment if there are errors
110+
echo "validation_error=$validationError" >> $GITHUB_OUTPUT
111+
if [[ "$validationError" == "true" ]]; then
112+
echo "VALIDATION_MESSAGES<<EOF" >> $GITHUB_ENV
113+
echo "$validationMessages" >> $GITHUB_ENV
114+
echo "EOF" >> $GITHUB_ENV
115+
echo "VALIDATION_HEADER=$VALIDATION_HEADER" >> $GITHUB_ENV
116+
fi
117+
env:
118+
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
119+
OWNER: ${{ github.repository_owner }}
120+
PR_NUMBER: ${{ github.event.number }}
121+
REPO: ${{ github.event.pull_request.base.repo.name }}
122+
123+
- name: Post Validation Comment
124+
if: steps.validate.outputs.validation_error == 'true'
125+
run: |
126+
# Check if a comment with the same validation messages already exists
127+
currentValidationHash=$(echo -n "$VALIDATION_MESSAGES" | sha256sum | cut -d' ' -f1)
128+
129+
existingComments="$(gh pr view $PR_NUMBER --repo $GITHUB_REPOSITORY --json comments --jq '.comments[].body' | grep -A 1000 "$VALIDATION_HEADER" | head -n -1 || echo "")"
130+
131+
duplicateFound=false
132+
if [[ -n "$existingComments" ]]; then
133+
while IFS= read -r comment; do
134+
if [[ -n "$comment" ]]; then
135+
# Extract validation messages from the existing comment (everything after the header until the footer)
136+
existingValidationMessages=$(echo "$comment" | sed -n "/^## $VALIDATION_HEADER$/,/^Please address these issues/p" | sed '1d;$d' | sed '/^$/d')
137+
if [[ -n "$existingValidationMessages" ]]; then
138+
existingValidationHash=$(echo -n "$existingValidationMessages" | sha256sum | cut -d' ' -f1)
139+
if [[ "$currentValidationHash" == "$existingValidationHash" ]]; then
140+
duplicateFound=true
141+
echo "Validation comment with identical messages already exists, skipping duplicate"
142+
break
143+
fi
144+
fi
145+
fi
146+
done <<< "$existingComments"
147+
fi
148+
149+
if [[ "$duplicateFound" == "false" ]]; then
150+
# Rate limiting delay
151+
sleep 2
152+
153+
commentBody="## $VALIDATION_HEADER"$'\n\n'"$VALIDATION_MESSAGES"$'\n'"Please address these issues and the validation will automatically re-run when you update your pull request."
154+
gh pr comment $PR_NUMBER --repo $GITHUB_REPOSITORY --body "$commentBody"
155+
echo "Posted validation comment successfully"
156+
fi
157+
env:
158+
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
159+
PR_NUMBER: ${{ github.event.number }}
160+
161+
- name: Check for Validation Errors
162+
if: steps.validate.outputs.validation_error == 'true'
163+
uses: actions/github-script@v7
164+
with:
165+
script: |
166+
core.setFailed('PR Formatting Validation Check Failed!')

0 commit comments

Comments
 (0)