1212 profiles/<pr-author-login>.md — named after the PR author's own
1313 GitHub username. Anything else is an error.
1414
15- Exit code is non-zero when any rule is broken; messages carry GitHub
16- Actions annotations so they show up right on the PR.
15+ Output goes to the console, to GitHub Actions annotations (::error),
16+ and — when running inside Actions — to the job summary page via
17+ $GITHUB_STEP_SUMMARY.
1718"""
1819
1920import argparse
21+ import os
2022import re
2123import sys
2224from pathlib import Path
2527MIN_CHARS = 20
2628ROOT = Path (__file__ ).resolve ().parent .parent
2729PROFILES = ROOT / "profiles"
30+ SUMMARY_PATH = os .environ .get ("GITHUB_STEP_SUMMARY" )
2831
2932
3033def note_error (message ):
3134 print ("::error::%s" % message .replace ("\n " , " " ))
3235
3336
37+ def summary (text ):
38+ if SUMMARY_PATH :
39+ with open (SUMMARY_PATH , "a" , encoding = "utf-8" ) as fh :
40+ fh .write (text + "\n " )
41+
42+
43+ def md_cell (text ):
44+ return text .replace ("|" , "\\ |" ).replace ("\n " , " " )
45+
46+
3447def check_file (path ):
3548 name = path .stem
3649 errors = []
@@ -42,40 +55,33 @@ def check_file(path):
4255 text = path .read_text (encoding = "utf-8" ).strip ()
4356 if len (text ) < MIN_CHARS :
4457 errors .append (
45- "%s: content is too short — write at least %d characters about "
46- "yourself (got %d)" % (path .name , MIN_CHARS , len (text ))
58+ "%s: content is too short — write at least %d characters "
59+ "about yourself (got %d)" % (path .name , MIN_CHARS , len (text ))
4760 )
4861 if not any (line .startswith ("# " ) for line in text .splitlines ()):
4962 errors .append ('%s: start the file with a "# Title" line' % path .name )
5063 return errors
5164
5265
53- def check_repo ():
54- """Layer 1: every profile in the repo is well-formed ."""
66+ def run_repo_check ():
67+ """Layer 1. Returns (ok, [(filename, errors)]) ."""
5568 if not PROFILES .is_dir ():
5669 note_error ("profiles/ directory is missing" )
57- return False
70+ return False , []
5871 files = sorted (
5972 p for p in PROFILES .glob ("*.md" ) if not p .name .startswith ("_" )
6073 )
61- if not files :
62- print ("profiles/ is empty — no profiles to check yet." )
63- return True
64-
65- ok = True
66- for path in files :
67- for message in check_file (path ):
74+ results = [(p .name , check_file (p )) for p in files ]
75+ for name , errors in results :
76+ for message in errors :
6877 note_error (message )
69- ok = False
70- if not check_file (path ):
71- print ("OK %s" % path .name )
72- print ("Repo check: %d profile(s) scanned%s"
73- % (len (files ), "" if ok else " — problems found above" ))
74- return ok
78+ if not errors :
79+ print ("OK %s" % name )
80+ print ("Repo check: %d profile(s) scanned" % len (results ))
81+ return all (not e for _ , e in results ), results
7582
7683
77- def check_pr (author , pr_files_path ):
78- """Layer 2: this PR adds exactly one file — profiles/<author>.md."""
84+ def read_pr_rows (pr_files_path ):
7985 rows = []
8086 for line in Path (pr_files_path ).read_text (encoding = "utf-8" ).splitlines ():
8187 if not line .strip ():
@@ -85,17 +91,21 @@ def check_pr(author, pr_files_path):
8591 rows .append ((parts [0 ], parts [1 ]))
8692 else :
8793 rows .append (("changed" , parts [0 ]))
94+ return rows
95+
8896
97+ def run_pr_check (author , rows ):
98+ """Layer 2. Returns (ok, info-dict)."""
8999 expected = "profiles/%s.md" % author
90100 errors = []
91-
92101 if len (rows ) != 1 :
93102 preview = ", " .join (name for _ , name in rows [:5 ]) or "nothing"
94103 errors .append (
95104 "a profile PR must change exactly ONE file — this one changes "
96105 "%d (%s). Keep the PR to your own profile only."
97106 % (len (rows ), preview )
98107 )
108+ status = rows [0 ][0 ] if len (rows ) == 1 else "multiple"
99109 else :
100110 status , filename = rows [0 ]
101111 if filename != expected :
@@ -104,20 +114,64 @@ def check_pr(author, pr_files_path):
104114 "your own GitHub username (@%s)"
105115 % (filename , expected , author )
106116 )
107- if status not in ( "added" ,) :
117+ if status != "added" :
108118 errors .append (
109- "%s must be a NEW file (GitHub reports status %r) — each "
110- "person adds exactly one profile; if it already exists, "
111- "edit your own file instead of adding it again"
119+ "%s must be a NEW file (GitHub reports status %r) — "
120+ "each person adds exactly one profile; if it already "
121+ "exists, edit your own file instead of adding it again"
112122 % (expected , status )
113123 )
114-
115124 for message in errors :
116125 note_error (message )
117126 if not errors :
118127 print ("PR check: adds exactly one new file, %s — matches author "
119128 "@%s." % (expected , author ))
120- return not errors
129+ return not errors , {
130+ "author" : author ,
131+ "expected" : expected ,
132+ "rows" : rows ,
133+ "errors" : errors ,
134+ }
135+
136+
137+ def write_summary (passed , repo_results , pr_info ):
138+ """Render the report onto the Actions job summary page."""
139+ summary ("## Profiles check — %s" % ("PASS ✅" if passed else "FAIL ❌" ))
140+ summary ("" )
141+
142+ summary ("### Repo profiles (%d)" % len (repo_results ))
143+ summary ("" )
144+ if repo_results :
145+ summary ("| Profile | Result |" )
146+ summary ("| --- | --- |" )
147+ for name , errors in repo_results :
148+ if errors :
149+ cell = "<br>" .join (md_cell (e ) for e in errors )
150+ summary ("| `%s` | ❌ %s |" % (md_cell (name ), cell ))
151+ else :
152+ summary ("| `%s` | ✅ |" % md_cell (name ))
153+ else :
154+ summary ("_profiles/ is empty — nothing to check yet._" )
155+ summary ("" )
156+
157+ if pr_info :
158+ summary ("### PR rules — author @%s" % pr_info ["author" ])
159+ summary ("" )
160+ summary ("Expected file: `%s`" % pr_info ["expected" ])
161+ summary ("" )
162+ summary ("| Status | File |" )
163+ summary ("| --- | --- |" )
164+ for status , filename in pr_info ["rows" ]:
165+ summary ("| %s | `%s` |" % (md_cell (status ), md_cell (filename )))
166+ if not pr_info ["rows" ]:
167+ summary ("| _none_ | _no changed files reported_ |" )
168+ summary ("" )
169+ if pr_info ["errors" ]:
170+ for message in pr_info ["errors" ]:
171+ summary ("- ❌ %s" % md_cell (message ))
172+ else :
173+ summary ("Exactly one new file, named after the PR author. ✅" )
174+ summary ("" )
121175
122176
123177def main ():
@@ -132,16 +186,24 @@ def main():
132186 )
133187 args = parser .parse_args ()
134188
135- ok = check_repo ()
189+ ok , repo_results = run_repo_check ()
136190
191+ pr_info = None
137192 if args .author and args .pr_files :
138- ok = check_pr (args .author , args .pr_files ) and ok
193+ pr_ok , pr_info = run_pr_check (
194+ args .author , read_pr_rows (args .pr_files )
195+ )
196+ ok = pr_ok and ok
139197 elif args .author or args .pr_files :
140198 note_error ("--author and --pr-files must be given together" )
141199 ok = False
142200
201+ write_summary (ok , repo_results , pr_info )
202+
143203 print ("" )
144204 print ("Profiles check: %s" % ("PASS" if ok else "FAIL" ))
205+ if SUMMARY_PATH :
206+ print ("Report also written to the job summary." )
145207 return 0 if ok else 1
146208
147209
0 commit comments