|
| 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