The docstring credited the every-15-minutes cron with putting Status/Blocked
back after a hand-removal. It cannot have: schedule and `issues` events run the
workflow from the default branch, weblib-archive's default branch is `main`, and
`main` still carries a stale pre-org-label-migration copy at
tools/sync_blocked_label.py. That copy looks the label up with
`repos/{repo}/labels`; both repos now have zero repo-level labels (all 25 are
org-level), so it returns None and exits "skipped: no 'Status/Blocked' label in
this repo" without writing anything. Measured today: 377 scheduled and 54
`issues` runs, every one a no-op.
The reassertions came from pull_request and workflow_dispatch runs on branches
cut from `development`, which consume the shared script. The symptom tracks PR
activity, not a timer -- which also explains why it looked intermittent.
Mechanism and fix are unchanged; only the attribution was wrong, and a wrong
cause in a permanent comment is what sends the next reader to the wrong file.
Co-authored-by: bit <bit@das-labor.org>
Closes weblib-ci#11 in part: the reconciler could add Status/Blocked but
never remove it when every blocker lived in another repo, and, found live
on weblib-archive#30 (2026-09-09), it went further and *reasserted* the
label over a human's hand-removal every 15 minutes.
Established which of the two candidate mechanisms in #11 was real by
instrumenting the script and running it under a real Gitea Actions
token scoped to weblib-archive, against weblib-archive#68 (a throwaway
issue depending on the already-closed weblib/cfbypass#1) and the live
weblib-archive#30: `deps` is never empty for a cross-repo edge (rules out
"Gitea omits the entry"), but every cross-repo entry comes back as Go's
zero-value Issue struct -- id 0, state "", repository null -- rather than
being omitted or erroring. The old `d.get("state") != "closed"` read that
blank state as "still open", which is why the label could never come off,
and why it kept getting silently reapplied.
`is_resolved()` keys on `id == 0` rather than on the empty `state`, since
`id` cannot legitimately be zero for a real issue and a future Gitea could
start populating `state` while still stubbing the rest of the entry.
`reconcile()` now keeps the existing add/remove behaviour whenever every
blocker resolves (same-repo dependencies always do; pinned by
TestSameRepoUnchanged so this cannot regress), but when a blocker cannot
be resolved and none of the resolvable ones are positively open, it
changes nothing and logs the issue number instead of guessing -- keeping
the asymmetry from cfbypass#19 (a false "unblocked" sends someone at work
that cannot be done; a false "blocked" only hides work) rather than
inverting it.
Also probed whether the repo-scoped token could resolve a cross-repo
blocker by querying the other repo directly instead of via
`/dependencies`: `GET repos/weblib/cfbypass/issues/1` and
`GET repos/weblib/cfbypass` both 404 under weblib-archive's own Actions
token, while the same call against its own repo succeeds. So this token
cannot see cfbypass at all; making cross-repo state resolvable would need
a broader-scoped credential, which is a real blast-radius decision left
for bit rather than assumed here. The "unknown -> leave alone, log it"
behaviour in this PR fixes the stuck/reasserted label either way, with or
without that credential ever existing.
12 new tests in test_sync_blocked_label.py, offline against a local
http.server fake. Proved by fault injection: reverting sync_blocked_label.py
to its pre-fix form (keeping the new tests) fails exactly the 5 tests in
TestUnresolvableCrossRepoBlocker -- one of them reproducing the live
reassert bug verbatim (`[(30, 'add', 'blocked by #0, #0')]`) -- while every
other test still passes; restoring the fix (diffed byte-identical against
the pre-revert copy) turns the suite green again.
python3 test_sync_blocked_label.py # 12 tests, OK
python3 test_report_job_log.py # 22 tests, OK (unaffected)
Co-authored-by: bit <bit@das-labor.org>
Two fixes. The first is a live regression I caused today; the second stops one
before it arms.
1. `label_id()` looked the label up in `repos/{repo}/labels` only. Labels moved
to the organisation today (weblib-archive#63) and that endpoint now returns
`[]` in all five repos, so it returned None everywhere and the whole script
became a silent no-op:
$ sync_blocked_label.py --repo weblib/weblib-archive --dry-run
skipped: no 'Status/Blocked' label in this repo
--> 0 change(s)
It failed *safe* - skipping rather than mislabelling, which is what that
docstring was written for - but a job that runs every 15 minutes reported
success while doing nothing. It now tries the repo, then the org, so it does
not care how an instance is arranged.
2. bit, 2026-09-07: "the reconciler must not touch issues that are already on
hold or abandoned". Implemented literally - neither add nor remove.
This matters because `Status/*` is becoming exclusive again. Under that,
adding `Status/Blocked` does not sit beside an existing status, it
*replaces* it - so the reconciler would silently delete a deliberate
`Status/On Hold` on its next pass. And since On Hold is exactly what makes
the backlog sweep skip an issue, a parked issue would quietly become an
available one, with nothing in the log to say why.
Verified against the live forge rather than by reading:
* add path, org-resolved: stripped Status/Blocked off cfbypass#8, dry-run
said "would add", the real run added it back
* hands-off, as a control on ONE issue with ONE open blocker, changing only
the label:
without Status/On Hold -> "would add Status/Blocked ... (blocked by #54)"
with Status/On Hold -> "hands off", 0 changes, label intact
Closes#3
Co-authored-by: bit <bit@das-labor.org>
Split out of the four repos per weblib-archive#44. All three files were
byte-identical across every repo at this moment, which will not stay true --
they converged only because four twin PRs landed within hours today, and
report_job_log.py had already drifted once before that.
Taken from weblib-archive, verified identical to every other copy first:
with-nixpkgs.sh ca43fa20 (cfbypass, archive, fs)
report_job_log.py aaef8f62 (cfbypass, archive)
sync_blocked_label.py e6ddb21d (all four)
action.yml is included so the `uses:` question can be re-measured now the repo
is public; it did not work while private.
Co-authored-by: bit <bit@das-labor.org>