Skip to content

Commit eb78de3

Browse files
committed
feat: report profiles check to ci summary
1 parent 812695a commit eb78de3

1 file changed

Lines changed: 93 additions & 31 deletions

File tree

‎scripts/check_profiles.py‎

Lines changed: 93 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
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

1920
import argparse
21+
import os
2022
import re
2123
import sys
2224
from pathlib import Path
@@ -25,12 +27,23 @@
2527
MIN_CHARS = 20
2628
ROOT = Path(__file__).resolve().parent.parent
2729
PROFILES = ROOT / "profiles"
30+
SUMMARY_PATH = os.environ.get("GITHUB_STEP_SUMMARY")
2831

2932

3033
def 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+
3447
def 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

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

Comments
 (0)