Skip to content

Commit 3e5c91a

Browse files
committed
Pre-review PR worflow
- github workflow - pre-review script - overall checks -> reviews contributions and changed files - insertion to CONTRIBUTORS.md
1 parent 3981a0f commit 3e5c91a

File tree

3 files changed

+296
-1
lines changed

3 files changed

+296
-1
lines changed

.github/workflows/pre-review.yml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
name: Pre-review
2+
3+
on:
4+
pull_request:
5+
pull_request_target:
6+
branches: [ master ]
7+
types: [opened, synchronize, reopened, closed]
8+
pull_request_review:
9+
types: [submitted]
10+
11+
permissions:
12+
contents: read
13+
pull-requests: write
14+
issues: write
15+
16+
env:
17+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18+
19+
jobs:
20+
21+
# -------------------------------
22+
# 2️⃣ Real PR info collection
23+
# -------------------------------
24+
pre_review:
25+
if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target'
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
with:
30+
repository: ${{ github.event.pull_request.head.repo.full_name }}
31+
ref: ${{ github.event.pull_request.head.ref }}
32+
fetch-depth: 0
33+
34+
- name: Install Python 3
35+
uses: actions/setup-python@v4
36+
with:
37+
python-version: 3.9
38+
39+
40+
- name: Fecth branch
41+
run: git fetch origin ${{ github.event.pull_request.base.ref }}
42+
43+
- name: Collect information
44+
run: |
45+
setEnv() { echo "$1=${!1}" >> $GITHUB_ENV; }
46+
47+
CONTRIBUTOR=${{ github.event.pull_request.user.login }}
48+
setEnv "CONTRIBUTOR"
49+
50+
BASE_REPO=${{ github.event.pull_request.base.repo.full_name }}
51+
setEnv "BASE_REPO"
52+
53+
HEAD_REF=${{ github.event.pull_request.head.ref }}
54+
setEnv "HEAD_REF"
55+
56+
PR_NUMBER=${{ github.event.pull_request.number }}
57+
setEnv "PR_NUMBER"
58+
echo "PR_NUMBER: $PR_NUMBER"
59+
60+
MERGE_BASE=$(git merge-base origin/${{ github.event.pull_request.base.ref }} HEAD)
61+
setEnv "MERGE_BASE"
62+
63+
CHANGED_FILES=$(gh pr view $PR_NUMBER -R "$BASE_REPO" --json files -q '.files[].path' | paste -sd '\\n' -)
64+
echo "CHANGED_FILES<<EOF" >> $GITHUB_ENV
65+
printf '%s\n' "$CHANGED_FILES" >> $GITHUB_ENV
66+
echo "EOF" >> $GITHUB_ENV
67+
68+
- name: Run "preReview" script
69+
run: |
70+
PR_NUMBER=${{ github.event.pull_request.number }}
71+
REVIEW_MESSAGE=$(python pre_review.py "$CHANGED_FILES" "$CONTRIBUTOR")
72+
echo "REVIEW_MESSAGE<<EOF" >> $GITHUB_ENV
73+
echo "$REVIEW_MESSAGE" >> $GITHUB_ENV
74+
echo "EOF" >> $GITHUB_ENV
75+
76+
# Save to env
77+
echo "REVIEW_MESSAGE<<EOF" >> $GITHUB_ENV
78+
echo "$REVIEW_MESSAGE" >> $GITHUB_ENV
79+
echo "EOF" >> $GITHUB_ENV
80+
81+
# Print to log explicitly
82+
echo "===== REVIEW_MESSAGE ====="
83+
echo "$REVIEW_MESSAGE"
84+
echo "=========================="
85+
86+
# Attempt to comment (will skip for forks)
87+
#if [ "${{ github.event.pull_request.head.repo.full_name }}" = "${{ github.repository }}" ]; then
88+
gh pr comment "$PR_NUMBER" --body "$REVIEW_MESSAGE" -R "${{ github.event.pull_request.base.repo.full_name }}"
89+
#fi
90+
91+
env:
92+
CHANGED_FILES: ${{ env.CHANGED_FILES }}
93+
CONTRIBUTOR: ${{ env.CONTRIBUTOR }}

CONTRIBUTORS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4426,6 +4426,8 @@
44264426

44274427
- [@Henri](https://github.com/MrHenryA)
44284428

4429+
- [@hussaindev94](https://github.com/hussaindev94)
4430+
44294431
- [@kemborah](https://github.com/kemborah)
44304432

44314433
- [@mehrdad9119](https://github.com/mehrdad9119)
@@ -5041,4 +5043,3 @@
50415043
- [@Cardinal117](https://github.com/Cardinal117)
50425044

50435045
- [@cyberDeeJay](https://github.com/cyberDeeJay)
5044-

pre_review.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
"""Pre review script: provides review on Pull Requests"""
2+
3+
import sys, json, re
4+
5+
6+
EXPECTED_FILE_CHANGED = 'CONTRIBUTORS.md'
7+
ERROR_FILE_WORD = "archived"
8+
README_LINK_MD = '[README.md](https://github.com/zero-to-mastery/start-here-guidelines/blob/master/README.md)'
9+
10+
# ---------------------------------- HELPERS --------------------------------- #
11+
def get_pr_info():
12+
"""Gets changed files into the correct format"""
13+
_, changed_files, contributor = sys.argv
14+
changed_files = changed_files.split("\\n")
15+
return changed_files, contributor
16+
17+
def get_check_patterns(contributor):
18+
"""Provides patterns criterions"""
19+
expected_url = f"https://github.com/{contributor}"
20+
21+
# Only match extra params (exclude exact URL)
22+
param_pattern = rf"{re.escape(expected_url)}/[^\s\)]+"
23+
24+
# Loose pattern: match either clean or with param (for general detection)
25+
loose_link_md_pattern = rf"\[\s*@?{re.escape(contributor)}\s*\]\(\s*https://github\.com/{re.escape(contributor)}(?:/[^\s\)]*)?\s*\)"
26+
27+
# Strict link pattern: matches only clean profile link
28+
strict_link_md_pattern = rf"\[\s*@{re.escape(contributor)}\s*\]\(\s*{re.escape(expected_url)}\/?\s*\)"
29+
30+
31+
# List item and full line contexts
32+
list_item_link_md_pattern = rf"^\s*-\s*\[\s*@?{re.escape(contributor)}\s*\]\(\s*{re.escape(expected_url)}(?:/[^\s\)]*)?\s*\)\s*$"
33+
34+
strict_line_md_pattern = rf"^\s*-\s*{strict_link_md_pattern}\s*$"
35+
36+
37+
return {
38+
"strict": strict_link_md_pattern,
39+
"loose": loose_link_md_pattern,
40+
"param": param_pattern,
41+
"list_item": list_item_link_md_pattern,
42+
"strict_line": strict_line_md_pattern,
43+
}
44+
45+
def match_pattern( pattern, content ):
46+
"""Regexp search operation"""
47+
return re.search(pattern, content, re.IGNORECASE | re.MULTILINE)
48+
49+
def push_feedback(base_message, action_message, review_list, is_inline = False):
50+
"""Format message and push to the provided list"""
51+
message_separator = "\n\t - " if not is_inline else ' '
52+
review_list.append(f'- [ ] {base_message}:{message_separator}{action_message}.')
53+
54+
# -------------------------------- LOGIC UTILS ------------------------------- #
55+
56+
def review_insertion( file_content, contributor ):
57+
"""Review CONTRIBUTORS.md for the github author"""
58+
# Format contributor to lowercase for checks
59+
contributor = contributor.lower()
60+
file_content = file_content.lower()
61+
patterns = get_check_patterns(contributor)
62+
63+
# Get pattern checks
64+
link_with_param = patterns["param"]
65+
loose_link_md_pattern = patterns["loose"]
66+
list_item_link_md_pattern = patterns["list_item"]
67+
strict_line_md_pattern = patterns["strict_line"]
68+
69+
70+
# Loosest to stricter match check
71+
has_link_param = match_pattern( link_with_param, file_content )
72+
has_md_loose_link = match_pattern( loose_link_md_pattern, file_content )
73+
has_md_list_item_link = match_pattern( list_item_link_md_pattern, file_content )
74+
has_md_valid_insertion = match_pattern( strict_line_md_pattern, file_content )
75+
76+
# Various insertion issues possible
77+
reviews = []
78+
if not has_md_valid_insertion:
79+
# [ CASE ] Param in github link
80+
if has_md_loose_link and has_link_param:
81+
base_message = 'Link has extra params'
82+
action = 'remove the url param from your link'
83+
push_feedback(base_message, action, reviews)
84+
85+
# [ CASES ] Link detected but with errors
86+
if has_md_loose_link:
87+
88+
# [ CASE ] Text link ( username text ) is missing `@`
89+
if '@' not in has_md_loose_link.group(0):
90+
base_message = 'Missing `@` before the github user name'
91+
action = 'prefix your github name with `@`'
92+
push_feedback(base_message, action, reviews)
93+
94+
# [ CASE ] Missing list item markdown bullet
95+
if not has_md_list_item_link:
96+
base_message = 'Missing hyphen (`-`) at the start of the line'
97+
action = 'prepend the line with an hyphen'
98+
push_feedback(base_message, action, reviews)
99+
100+
# [ CASES ] Insertion incorrect ( ex wrong protocol https, a typo, ... )
101+
else:
102+
base_message = 'Invalid insertion'
103+
action = 'review your line for any typos'
104+
push_feedback(base_message, action, reviews, True)
105+
106+
return reviews
107+
108+
def review_overall_contribution( changed_files ):
109+
"""Checks PR requirements"""
110+
111+
messages = []
112+
113+
# [ CASE ] Incorrect changes: missing changes int expected file
114+
if EXPECTED_FILE_CHANGED not in changed_files:
115+
push_feedback(
116+
"Missing required contribution",
117+
"add your github profile link to the contributors file",
118+
messages
119+
)
120+
121+
122+
# [ CASES ] Incorrect changes detected in other file(s)
123+
for file in changed_files:
124+
# other file than CONTRIBUTORS.md touched
125+
base_message = "Incorrect changes"
126+
action_message = f"remove any changes in the `{file}` file."
127+
128+
# "archived file touched"
129+
if ERROR_FILE_WORD in file:
130+
push_feedback(base_message, f"archived file:{action_message}", messages)
131+
break
132+
elif file != EXPECTED_FILE_CHANGED and file not in messages:
133+
push_feedback(base_message, action_message, messages)
134+
135+
136+
137+
# Title list of review
138+
if messages:
139+
messages = [
140+
f"> [!TIP] \n> _You can refer to ${README_LINK_MD} for additional guidance._",
141+
"\n ## Overall feedback",
142+
*messages
143+
]
144+
145+
return messages or []
146+
147+
def review_contributors_file( changed_files, contributor ):
148+
"""Reads CONTRIBUTORS.md file and checks insertion in CONTRIBUTORS.md"""
149+
if EXPECTED_FILE_CHANGED not in changed_files:
150+
return []
151+
152+
153+
# Gets file content
154+
content = ''
155+
with open(EXPECTED_FILE_CHANGED, 'r', encoding = 'utf-8') as f:
156+
content = f.read()
157+
158+
reviews = review_insertion( content, contributor )
159+
160+
if reviews:
161+
reviews = [
162+
'## `Contributors.md` addition feedback',
163+
'> [!TIP]',
164+
'> Check how others set their links to adjust yours if needed',
165+
*reviews
166+
]
167+
168+
return reviews or []
169+
170+
# -------------------------------- MAIN LOGIC -------------------------------- #
171+
def run():
172+
"""Main code"""
173+
changed_files, contributor = get_pr_info()
174+
175+
# Feedback for other files changes
176+
overall_reviews = review_overall_contribution(changed_files)
177+
178+
# Feedback regarding the expected line insertion
179+
insertion_reviews = review_contributors_file(changed_files, contributor)
180+
181+
reviews = [*overall_reviews, *insertion_reviews]
182+
if reviews:
183+
reviews = [
184+
"\nBefore we can merge your PR, address the bellow feedback.",
185+
'\n'.join([*overall_reviews, *insertion_reviews])
186+
]
187+
else:
188+
reviews = [
189+
"\n🎉 Your submission meets all pre-review requirements!",
190+
"It’s now awaiting final validation from a maintainer."
191+
]
192+
193+
messages = [
194+
f"Aloha @{contributor} 🙌",
195+
"Thanks for your contribution!",
196+
*reviews
197+
]
198+
message = '\n'.join(messages)
199+
print(message)
200+
201+
run()

0 commit comments

Comments
 (0)