Skip to content

Commit 227ceec

Browse files
committed
add PR title validation in CI
1 parent d7e454d commit 227ceec

5 files changed

Lines changed: 151 additions & 0 deletions

File tree

.github/PULL_REQUEST_TEMPLATE/task_submission_en.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
PR Title (CI enforced):
2+
- Tasks: `[TASK] <Task>-<Variant>. <Last Name> <First Name> <Middle Name>. <Group>. <Task name>.`
3+
- Development: `[DEV] <any descriptive title>`
4+
15
<!--
26
Pull request title requirement:
37

.github/PULL_REQUEST_TEMPLATE/task_submission_ru.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
Формат заголовка PR (CI):
2+
- Задачи: `[TASK] <Task>-<Variant>. <Фамилия> <Имя> <Отчество>. <Группа>. <Название задачи>.`
3+
- Разработка: `[DEV] <произвольное осмысленное название>`
4+
15
<!--
26
Требования к названию pull request:
37

.github/pull_request_template.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
PR Title (CI enforced):
2+
- Tasks: `[TASK] <Task>-<Variant>. <Last Name> <First Name> <Middle Name>. <Group>. <Task name>.`
3+
- Development: `[DEV] <any descriptive title>`
4+
15
<!-- Solution for PR template choice: https://stackoverflow.com/a/75030350/24543008 -->
26

37
Please go to the `Preview` tab and select the appropriate template:

.github/scripts/validate_pr.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
Minimal PR title validator for CI gate.
6+
7+
Rules:
8+
- Accept either a strict task title with required prefix '[TASK]'
9+
Pattern: [TASK] <Task>-<Variant>. <Last> <First> <Middle>. <Group>. <Task name>
10+
- Or a development title with prefix '[DEV]' followed by any non-empty text
11+
Pattern: [DEV] <any text>
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import argparse
17+
import json
18+
import os
19+
import re
20+
import sys
21+
from typing import List, Optional
22+
23+
24+
TITLE_TASK_REGEX = r"""
25+
^\[TASK\]\s+
26+
(?P<task>\d+)-(?P<variant>\d+)\.\s+
27+
(?P<lastname>[А-ЯA-ZЁ][а-яa-zё]+)\s+
28+
(?P<firstname>[А-ЯA-ZЁ][а-яa-zё]+)\s+
29+
(?P<middlename>[А-ЯA-ZЁ][а-яa-zё]+)\.\s+
30+
(?P<group>.+?)\.\s+
31+
(?P<taskname>\S.*)
32+
$
33+
"""
34+
35+
TITLE_DEV_REGEX = r"^\[DEV\]\s+\S.*$"
36+
37+
38+
def _trim(s: Optional[str]) -> str:
39+
return (s or "").strip()
40+
41+
42+
def validate_title(title: str) -> List[str]:
43+
"""Validate PR title. Returns a list of error messages (empty if valid)."""
44+
title = (title or "").strip()
45+
if not title:
46+
return [
47+
"Empty PR title. Use '[TASK] …' for tasks or '[DEV] …' for development.",
48+
]
49+
50+
# Accept development titles with a simple rule
51+
if re.match(TITLE_DEV_REGEX, title, flags=re.UNICODE):
52+
return []
53+
54+
# Accept strict course task titles
55+
if re.match(TITLE_TASK_REGEX, title, flags=re.UNICODE | re.VERBOSE):
56+
return []
57+
58+
example_task_ru = (
59+
"[TASK] 2-12. Иванов Иван Иванович. 2341-а234. Вычисление суммы элементов вектора."
60+
)
61+
example_task_en = (
62+
"[TASK] 2-12. Ivanov Ivan Ivanovich. 2341-a234. Vector elements sum calculation."
63+
)
64+
example_dev = "[DEV] Update docs for lab 2"
65+
return [
66+
"Invalid PR title.",
67+
"Allowed formats:",
68+
f"- Task: {example_task_ru}",
69+
f"- Task: {example_task_en}",
70+
f"- Dev: {example_dev}",
71+
]
72+
73+
74+
def _load_event_payload(path: Optional[str]) -> Optional[dict]:
75+
if not path or not os.path.exists(path):
76+
return None
77+
with open(path, "r", encoding="utf-8") as f:
78+
try:
79+
return json.load(f)
80+
except Exception:
81+
return None
82+
83+
84+
def main() -> int:
85+
try:
86+
payload = _load_event_payload(os.environ.get("GITHUB_EVENT_PATH"))
87+
88+
pr_title = None
89+
if payload and payload.get("pull_request"):
90+
pr = payload["pull_request"]
91+
pr_title = pr.get("title")
92+
93+
if pr_title is None:
94+
# Not a PR context – do not fail the gate
95+
print("No PR title in event payload; skipping title check (non-PR event).")
96+
return 0
97+
98+
errs = validate_title(pr_title)
99+
if errs:
100+
for e in errs:
101+
print(f"✗ {e}")
102+
return 1
103+
104+
print("OK: PR title is valid.")
105+
return 0
106+
107+
except SystemExit:
108+
raise
109+
except Exception as e:
110+
print(f"Internal error occurred: {e}", file=sys.stderr)
111+
return 2
112+
113+
114+
if __name__ == "__main__":
115+
sys.exit(main())

.github/workflows/main.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,31 @@ concurrency:
1616
!startsWith(github.ref, 'refs/heads/gh-readonly-queue') }}
1717
1818
jobs:
19+
pr_title:
20+
name: PR Title Gate
21+
runs-on: ubuntu-latest
22+
steps:
23+
- name: Non-PR event — skip title validation
24+
if: ${{ github.event_name != 'pull_request' }}
25+
run: echo "Not a PR event; skipping title check"
26+
27+
- name: Checkout
28+
if: ${{ github.event_name == 'pull_request' }}
29+
uses: actions/checkout@v4
30+
31+
- name: Set up Python
32+
if: ${{ github.event_name == 'pull_request' }}
33+
uses: actions/setup-python@v5
34+
with:
35+
python-version: '3.11'
36+
37+
- name: Validate PR title
38+
if: ${{ github.event_name == 'pull_request' }}
39+
run: python .github/scripts/validate_pr.py
40+
1941
pre-commit:
42+
needs:
43+
- pr_title
2044
uses: ./.github/workflows/pre-commit.yml
2145
ubuntu:
2246
needs:

0 commit comments

Comments
 (0)