ctf_status.py is now just the command line; the work lives in tracker/. The rules that decide whether a challenge is believed are in tracker/checks.py, one function per rule, listed in README_CHECKS and REPO_CHECKS. Output is unchanged. Read the config from .env only (ctf_status.env.example -> .env.example), let single_run_lock() hold its own lock files, and rewrite the README as a short overview of what the repo does and how to run it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
5.2 KiB
Python
115 lines
5.2 KiB
Python
"""The HedgeDoc note, as markdown."""
|
||
from __future__ import annotations
|
||
|
||
from collections import Counter
|
||
from datetime import datetime
|
||
|
||
from .challenge import BLOCKED_ICON, STATUS_ICON, STATUS_ORDER
|
||
from .overview import Overview, icons
|
||
|
||
|
||
def md_cell(v: str) -> str:
|
||
return v.replace("|", "\\|")
|
||
|
||
|
||
def render_hedgedoc(ov: Overview, title: str, tags: str = "") -> str:
|
||
stamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
|
||
stages = ov.stages
|
||
groups = ov.by_category()
|
||
total = Counter(ch.stage for ch in ov.challenges)
|
||
L = []
|
||
if tags:
|
||
# HedgeDoc reads this YAML block off the top of the note
|
||
L += ["---", f"tags: {tags}", "---", ""]
|
||
L += [f"# {title}", "",
|
||
f"**Last updated:** {stamp}", "",
|
||
f"*generated by `ctf_status.py` from the README.md of every repo in the "
|
||
f"GitLab challenges group — {len(ov.challenges)} challenges*", "",
|
||
" · ".join(f"{STATUS_ICON[s]} **{s or 'unknown'}** {total[s]}"
|
||
for s in stages if total[s] or s), ""]
|
||
|
||
ready = ov.testable
|
||
L += [f"## ✅ Ready for infra testing ({len(ready)} of {len(ov.challenges)})", "",
|
||
"*every challenge at Testing Ready or beyond whose repo backs that up — "
|
||
"these can be deployed and tested against the infrastructure now*", ""]
|
||
if ready:
|
||
L += ["| | Challenge | Category | Difficulty | Status | Event | Author | Tester |",
|
||
"| --- | --- | --- | --- | --- | --- | --- | --- |"]
|
||
for ch in ready:
|
||
L.append("| " + " | ".join(md_cell(x) for x in (
|
||
STATUS_ICON[ch.stage], f"[{ch.title}]({ch.repo.web_url})",
|
||
ch.category or "?", ch.difficulty or "?", ch.stage, ch.get("Event"),
|
||
ch.get("Author"), ch.get("Tester") if ch.has_tester else "—")) + " |")
|
||
else:
|
||
L.append("*nothing yet*")
|
||
L.append("")
|
||
|
||
if ov.blocked:
|
||
L += [f"## {BLOCKED_ICON} Claims Testing Ready+, but the repo disagrees "
|
||
f"({len(ov.blocked)})", "",
|
||
"*the README says so, but description.yml or the repo shows it is not "
|
||
"finished — kept out of challs.txt until this is fixed*", ""]
|
||
for ch in ov.blocked:
|
||
L.append(f"- {STATUS_ICON[ch.stage]} [{md_cell(ch.title)}]({ch.repo.web_url}) "
|
||
f"({ch.stage}): " + "; ".join(ch.blockers))
|
||
L.append("")
|
||
|
||
L += ["## Status by category", "",
|
||
"| Category | " + " | ".join(f"{STATUS_ICON[s]} {s or '?'}" for s in stages)
|
||
+ " | total |",
|
||
"| --- |" + " ---: |" * (len(stages) + 1)]
|
||
for cat in ov.categories:
|
||
chs = groups.get(cat, [])
|
||
cnt = Counter(ch.stage for ch in chs)
|
||
L.append(f"| {cat} | " + " | ".join(str(cnt[s] or "·") for s in stages)
|
||
+ f" | **{len(chs)}** |")
|
||
L.append("| **total** | " + " | ".join(f"**{total[s]}**" for s in stages)
|
||
+ f" | **{len(ov.challenges)}** |")
|
||
|
||
diffs = ov.difficulties
|
||
L += ["", "## Category × difficulty", "",
|
||
"*one dot per challenge, coloured by status: "
|
||
+ " ".join(f"{STATUS_ICON[s]} {s}" for s in STATUS_ORDER)
|
||
+ f" {BLOCKED_ICON} blocked*", "",
|
||
"| Category | " + " | ".join(diffs) + " | total |",
|
||
"| --- |" + " --- |" * len(diffs) + " ---: |"]
|
||
for cat in ov.categories:
|
||
chs = groups.get(cat, [])
|
||
cells = [icons([ch for ch in chs if (ch.difficulty or "?") == d]) or "·" for d in diffs]
|
||
L.append(f"| {cat} | " + " | ".join(cells) + f" | **{len(chs)}** |")
|
||
dtot = Counter(ch.difficulty or "?" for ch in ov.challenges)
|
||
L.append("| **total** | " + " | ".join(f"**{dtot[d]}**" for d in diffs)
|
||
+ f" | **{len(ov.challenges)}** |")
|
||
|
||
L += ["", "## Challenges"]
|
||
for cat in ov.categories:
|
||
chs = groups.get(cat)
|
||
if not chs:
|
||
continue
|
||
L += ["", f"### {cat} ({len(chs)})", "",
|
||
"| | Challenge | Difficulty | Status | Event | Author | Tester | Last activity |",
|
||
"| --- | --- | --- | --- | --- | --- | --- | --- |"]
|
||
for ch in chs:
|
||
status = ch.stage or "?"
|
||
if ch.status_raw.lower() != status.lower():
|
||
status += f" *({ch.status_raw})*"
|
||
cat_raw = ch.get("Category", "")
|
||
name = f"[{md_cell(ch.title)}]({ch.repo.web_url})"
|
||
if cat_raw.lower() != cat:
|
||
name += f" *({cat_raw})*"
|
||
L.append("| " + " | ".join(md_cell(x) for x in (
|
||
ch.icon, name, ch.difficulty or ch.get("Difficulty"), status,
|
||
ch.get("Event"), ch.get("Author"),
|
||
ch.get("Tester") if ch.has_tester else "—", ch.repo.last_activity)) + " |")
|
||
|
||
L += ["", "## Needs attention", ""]
|
||
issues = [ch for ch in sorted(ov.challenges, key=lambda c: c.repo.path.lower()) if ch.problems]
|
||
for ch in issues:
|
||
L.append(f"- [{md_cell(ch.title)}]({ch.repo.web_url}): " + "; ".join(ch.problems))
|
||
for ch in ov.blank:
|
||
why = "no README" if ch.repo.readme is None else "README is still the blank template"
|
||
L.append(f"- [{ch.repo.path}]({ch.repo.web_url}): {why}")
|
||
if not issues and not ov.blank:
|
||
L.append("*nothing — every README is filled in. suspicious.*")
|
||
return "\n".join(L) + "\n"
|