Source challenge status from GitLab instead of Nextcloud

Read the README.md of every repo in the GitLab challenges group via the
REST API and rework the HedgeDoc note for project tracking: challenges
ready for infra testing first, then status by category and a
category x difficulty grid. Free-text status/difficulty/category values
are normalised; untouched templates and repos scheduled for deletion
are left out of the counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 16:12:35 +02:00
co-authored by Claude Opus 5.5
parent 7c546ab673
commit e8a04fe415
6 changed files with 423 additions and 291 deletions
+53 -30
View File
@@ -1,13 +1,14 @@
# ctf_status.py
Reads the per-author challenge folders straight off the Nextcloud share (WebDAV,
no sync, no mount) and tells you who to scream at.
Reads the `README.md` of every repo in the GitLab challenges group (REST API, no
clone) and shows which categories have challenges in which state, at which
difficulty — in the terminal and as a HedgeDoc note.
## Setup
```sh
cp ctf_status.env.example ctf_status.env && chmod 600 ctf_status.env
$EDITOR ctf_status.env # app password from Nextcloud → Settings → Security
$EDITOR ctf_status.env # GitLab group URL + token, HedgeDoc note
```
## Use
@@ -20,6 +21,10 @@ $EDITOR ctf_status.env # app password from Nextcloud → Settings → Secur
./ctf_status.py --update <note-url> # ... or an explicit one
```
The token is a fine-grained personal access token on the challenges group with
**Project: Read** (to list the repos) and **Repository: Read** (to download the
READMEs). A plain `.env` with `GL_PAT=` / `GITLAB_URL=` is read as well.
## Keeping a HedgeDoc note up to date
`--update` overwrites an **existing** note in place, so the URL you share stays the
@@ -78,33 +83,51 @@ changed or something broke. Overlapping runs are prevented by a lock file, and t
HedgeDoc write is retried (`--retries`, default 3) before the run gives up with a
one-line error and a non-zero exit.
## What the note shows
1. **✅ Ready for infra testing** — first thing in the note: every challenge at
Testing Ready, Tested or Ready, linked to its repo, so infra knows what it
can deploy and test against right now.
2. **Status by category** — how many challenges each category has per stage
(⚪ Planning → 🟠 PoC Draft → 🟡 Testing Ready → 🔵 Tested → 🟢 Ready).
3. **Category × difficulty** — one dot per challenge, coloured by its status, so
gaps and stragglers show up at a glance.
4. **Challenges** — one table per category, linked to the repo, with event,
author, tester and the repo's last activity.
5. **Needs attention** — see below.
## Reading the README
Only the template's header block is parsed:
```
### Challenge Name
- Category: crypto
- Difficulty: medium
- Event: TUM student CTF
- Author: ...
- Tester: ...
- Status: Testing Ready
```
People fill it in loosely, so values are normalised:
- **Status** is matched against the five stages ("Tested with …" → Tested,
"PoC" → PoC Draft). If it names several ("Planning to PoC Draft", a half-deleted
option list) the *least* advanced one counts and it is flagged as unclear.
- **Difficulty** ranges written as `medium-hard`, `medium/hard` or
`easy to medium` become their own column; otherwise the first level named wins.
- **Category**: the first one named counts ("rev, misc" → rev); the full value
is shown next to the challenge. Categories outside the usual list (e.g. `ai`)
get their own row.
## What it flags
| verdict | meaning |
| --- | --- |
| 🔴 SCREAM | nothing submitted, **or** they filled out `template.md` in place |
| 🟡 NAG | submitted, but placeholders / empty sections remain |
| 🟢 OK | every challenge fully filled out |
- challenge at Testing Ready or beyond with no tester (`[Tester Name]`, `??`,
`None`, `not found yet` all count as none)
- status / difficulty / category missing or unclear
- no challenge name in the README
- repos whose README is missing or still the untouched template — these are left
out of the counts
A `template.md` that is still pristine is ignored. One that has real content in it
is counted as a challenge *and* flagged — so nobody's work is lost, they just get
yelled at.
Also flagged per challenge: unnamed challenge, `[placeholder]` values left in any
of Category/Difficulty/Event/Author/Tester/Status, empty prose sections, and
category/difficulty values outside the allowed set.
## Layout it expects
```
<NC_ROOT>/
alice/
README.md # ignored
template.md # ignored while pristine
baby-rsa.md # ← a challenge, any filename
bob/
```
Any `.md` that isn't `README.md` is parsed as a challenge, so the file naming
convention doesn't have to be enforced by the script. If you want it enforced,
add the check in `Challenge.problems`.
Repos GitLab has scheduled for deletion, and archived repos, are skipped.