forked from electronicarts/CnC_Generals_Zero_Hour
-
Notifications
You must be signed in to change notification settings - Fork 170
186 lines (159 loc) · 6.99 KB
/
validate-pull-request.yml
File metadata and controls
186 lines (159 loc) · 6.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
name: Validate Pull Request
# Minimal permissions: read code, write PR comments.
permissions:
contents: read
pull-requests: write
# Uses pull_request_target to access secrets for PR comments on forks.
on:
pull_request_target:
branches:
- main
types:
- opened
- edited
- synchronize
- reopened
# Avoid racing on PR comments when multiple events fire quickly.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
validate-title-and-commits:
name: Validate Title and Commits
runs-on: ubuntu-slim
timeout-minutes: 3
# Expose context as env vars to avoid inline ${{ }} in run blocks (injection hardening).
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_REF: ${{ github.base_ref }}
REPO: ${{ github.repository }}
steps:
- name: Checkout base branch
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
fetch-depth: 0
- name: Fetch PR head
run: git fetch origin "$PR_HEAD_SHA"
- name: Load valid tags
id: load-tags
run: |
TAGS_FILE=".github/workflows/valid-tags.txt"
if [ ! -f "$TAGS_FILE" ]; then
echo "::error::$TAGS_FILE file not found"
exit 1
fi
# Normalize line endings and remove empty lines
TAGS=$(tr -d '\r' < "$TAGS_FILE" | sed '/^$/d')
VALID_TAGS=$(echo "$TAGS" | tr '\n' ',' | sed 's/,$//; s/,/, /g')
echo "**Valid tags**: $VALID_TAGS" >> "$GITHUB_STEP_SUMMARY"
echo "valid-tags=$VALID_TAGS" >> "$GITHUB_OUTPUT"
TAG_REGEX=$(echo "$TAGS" | paste -sd "|" -)
# Matches:
# Conventional commit: type or type(scope) followed by colon, single space, then uppercase text
REGEX="^(($TAG_REGEX)(\\([^)]+\\))?: [A-Z].*)$"
echo "regex=$REGEX" >> "$GITHUB_OUTPUT"
echo "Built the regex: $REGEX"
- name: Validate PR title
id: validate-title
env:
REGEX: ${{ steps.load-tags.outputs.regex }}
run: |
echo "### Validate PR Title" >> "$GITHUB_STEP_SUMMARY"
TITLE=$(jq -r '.pull_request.title // "No title found"' "$GITHUB_EVENT_PATH")
if [[ ! "$TITLE" =~ $REGEX ]]; then
echo "- ❌ PR title \"$TITLE\" is invalid." >> "$GITHUB_STEP_SUMMARY"
echo "title-valid=false" >> "$GITHUB_OUTPUT"
DELIM="TITLE_EOF_$(openssl rand -hex 8)"
{
echo "INVALID_TITLE<<$DELIM"
echo "$TITLE"
echo "$DELIM"
} >> "$GITHUB_ENV"
else
echo "- ✅ PR title \"$TITLE\" is valid." >> "$GITHUB_STEP_SUMMARY"
echo "title-valid=true" >> "$GITHUB_OUTPUT"
fi
- name: Validate PR commits
id: validate-commits
if: (success() || failure()) && steps.load-tags.outcome == 'success'
env:
REGEX: ${{ steps.load-tags.outputs.regex }}
run: |
echo "### Validate PR Commits" >> "$GITHUB_STEP_SUMMARY"
COMMITS=$(git log "$BASE_REF".."$PR_HEAD_SHA" --pretty=format:"%s" --no-merges)
if [[ -z "$COMMITS" ]]; then
echo "- ⚠️ No non-merge commits found." >> "$GITHUB_STEP_SUMMARY"
echo "commits-valid=true" >> "$GITHUB_OUTPUT"
exit 0
fi
INVALID_COMMITS=0
INVALID_LIST=""
while IFS= read -r COMMIT_MSG; do
if [[ -z "$COMMIT_MSG" ]]; then
continue
fi
if [[ ! "$COMMIT_MSG" =~ $REGEX ]]; then
echo "- ❌ Commit message \"$COMMIT_MSG\" is invalid." >> "$GITHUB_STEP_SUMMARY"
INVALID_COMMITS=$((INVALID_COMMITS + 1))
SANITIZED_MSG=$(echo "$COMMIT_MSG" | tr -d '\`')
INVALID_LIST="${INVALID_LIST}- \`${SANITIZED_MSG}\`"$'\n'
else
echo "- ✅ Commit message \"$COMMIT_MSG\" is valid." >> "$GITHUB_STEP_SUMMARY"
fi
done <<< "$COMMITS"
if [[ $INVALID_COMMITS -gt 0 ]]; then
echo "commits-valid=false" >> "$GITHUB_OUTPUT"
DELIM="COMMITS_EOF_$(openssl rand -hex 8)"
{
echo "INVALID_COMMITS_LIST<<$DELIM"
printf '%s' "$INVALID_LIST"
echo "$DELIM"
} >> "$GITHUB_ENV"
else
echo "commits-valid=true" >> "$GITHUB_OUTPUT"
fi
# Always clean up old failure comments, even when validation now passes.
- name: Delete stale bot comments
if: always() && steps.load-tags.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api --paginate "repos/$REPO/issues/$PR_NUMBER/comments" \
--jq '.[] | select(.user.login == "github-actions[bot]" and (.body | startswith("### ⚠️ Title/Commit Validation Failed"))) | .id' \
| while read -r comment_id; do
gh api -X DELETE "repos/$REPO/issues/comments/$comment_id" || true
done || true
# Post a new failure comment with details on what's wrong.
- name: Comment on PR if validation failed
if: always() && steps.load-tags.outcome == 'success' && (steps.validate-title.outputs.title-valid != 'true' || steps.validate-commits.outputs.commits-valid != 'true')
env:
GH_TOKEN: ${{ github.token }}
VALID_TAGS_RAW: ${{ steps.load-tags.outputs.valid-tags }}
run: |
VALID_TAGS=$(echo "$VALID_TAGS_RAW" | sed 's/[^, ][^, ]*/`&`/g')
BODY="### ⚠️ Title/Commit Validation Failed"
if [[ -n "$INVALID_TITLE" ]]; then
SANITIZED_TITLE=$(echo "$INVALID_TITLE" | tr -d '\`')
BODY="$BODY"$'\n\n'"**Invalid PR title:**"
BODY="$BODY"$'\n'"- \`$SANITIZED_TITLE\`"
fi
if [[ -n "$INVALID_COMMITS_LIST" ]]; then
BODY="$BODY"$'\n\n'"**Invalid commit messages:**"
BODY="$BODY"$'\n'"$INVALID_COMMITS_LIST"
fi
BODY="$BODY"$'\n'"PR titles and commit messages must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) format:"
BODY="$BODY"$'\n'"\`\`\`"
BODY="$BODY"$'\n'"type: Description"
BODY="$BODY"$'\n'"type(scope): Description"
BODY="$BODY"$'\n'"\`\`\`"
BODY="$BODY"$'\n\n'"**Allowed types:** $VALID_TAGS"
BODY="$BODY"$'\n\n'"See [CONTRIBUTING.md](https://github.com/$REPO/blob/$BASE_REF/CONTRIBUTING.md#pull-request-documentation) for details."
gh pr comment "$PR_NUMBER" \
--repo "$REPO" \
--body "$BODY"
# Separate fail step so the comment is always posted before the job fails.
- name: Fail if validation did not pass
if: always() && steps.load-tags.outcome == 'success' && (steps.validate-title.outputs.title-valid != 'true' || steps.validate-commits.outputs.commits-valid != 'true')
run: exit 1