1+ name : " PR Title check worker"
2+
3+ on :
4+ pull_request :
5+ types : [opened, edited, reopened, ready_for_review, synchronize]
6+
7+ jobs :
8+ validate-pr-title :
9+ runs-on : ubuntu-latest
10+ continue-on-error : true
11+ permissions :
12+ contents : read
13+ checks : write
14+
15+ steps :
16+ - name : Generate a token
17+ id : generate-token
18+ uses : actions/create-github-app-token@v2
19+ with :
20+ app-id : ${{ secrets.OPENAEV_PR_CHECKS_APP_ID }}
21+ private-key : ${{ secrets.OPENAEV_PR_CHECKS_PRIVATE_KEY }}
22+ - name : Validate PR title and create check
23+ shell : bash
24+ env :
25+ GITHUB_TOKEN : ${{ steps.generate-token.outputs.token }}
26+ REPO : ${{ github.repository }}
27+ SHA : ${{ github.event.pull_request.head.sha }}
28+ run : |
29+ set -euo pipefail
30+
31+ TITLE="${{ github.event.pull_request.title }}"
32+ echo "PR title: $TITLE"
33+
34+ # Skip validation for renovate
35+ if [[ "$TITLE" == *"chore(deps)"* ]]; then
36+ echo "⚠️ Skipping validation for renovate PRs."
37+ OUTPUT_TITLE="⚠️ Skipping validation for Renovate PRs."
38+ OUTPUT_SUMMARY="⚠️ Skipping validation for Renovate PRs."
39+ CONCLUSION="success"
40+ else
41+ # Full pattern:
42+ # [category/subcategory] type(scope?): description (#123)
43+ FULL_PATTERN='^\[([a-z]+(/[a-z]+)*)\] (feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\([a-z-]+\))?: [a-z].*( \(#[0-9]+\))$'
44+
45+ if [[ "$TITLE" =~ $FULL_PATTERN ]]; then
46+ echo "✅ PR title is valid."
47+ OUTPUT_TITLE="✅ PR title is valid."
48+ OUTPUT_SUMMARY="✅ PR title is valid."
49+ CONCLUSION="success"
50+ else
51+ # Diagnose common failures
52+
53+ # 1) Check category block: [category/category]
54+ CATEGORY_PATTERN='^\[([a-z]+(/[a-z]+)*)\]'
55+ if ! [[ "$TITLE" =~ $CATEGORY_PATTERN ]]; then
56+ REASON="Bad [category] block. Expected: [category] or [category/category]"
57+ fi
58+
59+ # 2) Check type + optional scope
60+ TYPE_PATTERN='^\[([a-z]+(/[a-z]+)*)\] (feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\([a-z-]+\))?: '
61+ if [[ -z "${REASON:-}" ]] && ! [[ "$TITLE" =~ $TYPE_PATTERN ]]; then
62+ REASON="Bad type(scope): block. Expected type: feat, fix, chore, docs, style, refactor, perf, test, build, ci, revert (optionally with scope: type(scope):)"
63+ fi
64+
65+ # 3) Check description starts with lowercase letter
66+ DESC_PATTERN='^\[([a-z]+(/[a-z]+)*)\] (feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\([a-z-]+\))?: [a-z]'
67+ if [[ -z "${REASON:-}" ]] && ! [[ "$TITLE" =~ $DESC_PATTERN ]]; then
68+ REASON="Bad description. Must start with a lowercase letter after ': '"
69+ fi
70+
71+ # 4) Check issue reference at the end: (#XXX)
72+ ISSUE_PATTERN='\(#[0-9]+\)$'
73+ if [[ -z "${REASON:-}" ]] && ! [[ "$TITLE" =~ $ISSUE_PATTERN ]]; then
74+ REASON="Bad (#XXX) ending block. Missing issue reference"
75+ fi
76+
77+ if [[ -z "${REASON:-}" ]]; then
78+ REASON="Bad title. Does not match the required pattern"
79+ fi
80+
81+ echo "❌ Invalid PR title: $REASON"
82+ echo "Required format:"
83+ echo "[category] type(scope?): description (#123)"
84+
85+ OUTPUT_TITLE="$REASON"
86+ OUTPUT_SUMMARY="❌ Invalid PR title: $REASON. \nRequired: [category] type(scope?): description (#XXX)"
87+ CONCLUSION="failure"
88+ fi
89+ fi
90+
91+ # Create custom check run
92+ CHECK_RUN=$(
93+ curl -sS -X POST \
94+ -H "Authorization: Bearer $GITHUB_TOKEN" \
95+ -H "Accept: application/vnd.github+json" \
96+ https://api.github.com/repos/$REPO/check-runs \
97+ -d @- <<EOF
98+ {
99+ "name": "Validate PR Title (optional)",
100+ "head_sha": "$SHA",
101+ "status": "in_progress"
102+ }
103+ EOF
104+ )
105+
106+ CHECK_RUN_ID=$(echo "$CHECK_RUN" | jq -r '.id')
107+ echo "Created check run ID: $CHECK_RUN_ID"
108+
109+ # Complete the check run with conclusion + detailed summary
110+ curl -sS -X PATCH \
111+ -H "Authorization: Bearer $GITHUB_TOKEN" \
112+ -H "Accept: application/vnd.github+json" \
113+ https://api.github.com/repos/$REPO/check-runs/$CHECK_RUN_ID \
114+ -d @- <<EOF
115+ {
116+ "status": "completed",
117+ "conclusion": "$CONCLUSION",
118+ "output": {
119+ "title": "$OUTPUT_TITLE",
120+ "summary": "$OUTPUT_SUMMARY"
121+ }
122+ }
123+ EOF
124+
125+ # Do not fail job (continue-on-error is true)
126+ exit 0
0 commit comments