Reconciler can never clear Status/Blocked when every blocker is in another repo #11

Open
opened 2026-09-08 08:34:22 +00:00 by claude · 0 comments
Member

sync_blocked_label.py cannot take Status/Blocked off an issue whose blockers all live in another repo. It adds correctly and it never removes, so the label sticks forever and the backlog sweep keeps reporting the issue as blocked after it is free.

The natural experiment, 2026-09-08

Two issues had every blocker closed within minutes of each other. The reconciler runs on a */15 cron in each repo and reported success on every run.

issue blockers where label after 4 scheduled runs
cfbypass#1 its own sub-issues, all closed same repo removed automatically at ~07:44
weblib-archive#30 cfbypass#1, cfbypass#33, both closed cross repo still Status/Blocked at 08:30

Runs 1171/1176/1179/1182 (07:45, 08:00, 08:15, 08:30) all green. HANDS_OFF does not apply — #30 carries Kind/Feature, Priority/Low, Status/Blocked, Type/Epic, and neither Status/On Hold nor Status/Abandoned. I removed the label by hand to unstick the board, so the reproduction is gone from #30; re-create it by adding a cross-repo dependency that is already closed.

Two candidate mechanisms, both consistent, not yet distinguished

The Actions token is scoped to the repo it runs in, so it cannot read cfbypass while running in weblib-archive. Either:

  1. Gitea omits the cross-repo entries from repos/{repo}/issues/{n}/dependencies for a token that cannot see the other repo. deps then comes back empty and the function takes its "no edges, no opinion" path and continues — never touching the label. I think this is the likelier one, because it also explains the silence: an empty list produces no log line at all.
  2. The entries are returned without a resolvable state. Then d.get("state") != "closed" is true, every cross-repo blocker counts as open forever, and the label is re-asserted rather than removed.

They are easy to tell apart: log len(deps) and the raw entries for one known cross-repo case under the Actions token. Worth doing before fixing, since the two need different fixes — 1 needs a token that can see the other repo, 2 needs the state resolved by querying the blocker's own repo directly.

Why this is the fail-safe from #19 biting back

This is not a regression, it is the deliberate trade from cfbypass#19. Before that fix the rule was "only a positively open blocker blocks", and an unreadable cross-repo entry read as not-open — so the runner took the label off while cfbypass#1 was still open. The fix inverted it to "only a positively closed blocker is done", which is the right direction: a false "unblocked" sends someone to work on something that cannot be worked on, while a false "blocked" only hides available work.

But the inverted rule has no exit. Under mechanism 2 a cross-repo blocker can never be confirmed closed, so the label is permanent. #19 fixed the dangerous half and left the stuck half, and nothing measured the stuck half because the only cross-repo blockers at the time were still open.

Why it matters beyond one label

The backlog sweep in docs/UNSUPERVISED.md keys on Status/Blocked and on the dependency graph. A permanently-labelled issue is one that never comes back as workable — the exact failure bit called out on 2026-09-07: "they were unblocked/etc. and you should have started working on them". weblib-archive#30 is the largest item on the board and it would have stayed invisible.

Note the cross-repo case is not an edge case here: weblib-archive#30 → cfbypass#1, #29 → weblib-fs#17, #57#30. The dependency graph in this org is mostly cross-repo.

Adjacent

Whatever the fix, it wants a test, and #9 is that nothing runs this repo's tests at all. The reporting side of the same problem: a job that does nothing still reports success, which is how this survived four green runs.

`sync_blocked_label.py` cannot take `Status/Blocked` **off** an issue whose blockers all live in another repo. It adds correctly and it never removes, so the label sticks forever and the backlog sweep keeps reporting the issue as blocked after it is free. ## The natural experiment, 2026-09-08 Two issues had every blocker closed within minutes of each other. The reconciler runs on a `*/15` cron in each repo and reported **success** on every run. | issue | blockers | where | label after 4 scheduled runs | |---|---|---|---| | cfbypass#1 | its own sub-issues, all closed | **same repo** | **removed automatically** at ~07:44 | | weblib-archive#30 | cfbypass#1, cfbypass#33, both closed | **cross repo** | **still `Status/Blocked`** at 08:30 | Runs 1171/1176/1179/1182 (07:45, 08:00, 08:15, 08:30) all green. `HANDS_OFF` does not apply — #30 carries `Kind/Feature`, `Priority/Low`, `Status/Blocked`, `Type/Epic`, and neither `Status/On Hold` nor `Status/Abandoned`. I removed the label by hand to unstick the board, so **the reproduction is gone from #30**; re-create it by adding a cross-repo dependency that is already closed. ## Two candidate mechanisms, both consistent, not yet distinguished The Actions token is scoped to the repo it runs in, so it cannot read cfbypass while running in weblib-archive. Either: 1. **Gitea omits the cross-repo entries** from `repos/{repo}/issues/{n}/dependencies` for a token that cannot see the other repo. `deps` then comes back empty and the function takes its "no edges, no opinion" path and `continue`s — never touching the label. **I think this is the likelier one**, because it also explains the silence: an empty list produces no log line at all. 2. **The entries are returned without a resolvable `state`.** Then `d.get("state") != "closed"` is true, every cross-repo blocker counts as open forever, and the label is re-asserted rather than removed. They are easy to tell apart: log `len(deps)` and the raw entries for one known cross-repo case under the Actions token. Worth doing before fixing, since the two need different fixes — 1 needs a token that can see the other repo, 2 needs the state resolved by querying the blocker's own repo directly. ## Why this is the fail-safe from #19 biting back This is not a regression, it is the deliberate trade from cfbypass#19. Before that fix the rule was "only a positively *open* blocker blocks", and an unreadable cross-repo entry read as not-open — so the runner **took the label off while cfbypass#1 was still open**. The fix inverted it to "only a positively *closed* blocker is done", which is the right direction: a false "unblocked" sends someone to work on something that cannot be worked on, while a false "blocked" only hides available work. But the inverted rule has no exit. Under mechanism 2 a cross-repo blocker can never be confirmed closed, so the label is permanent. #19 fixed the dangerous half and left the stuck half, and nothing measured the stuck half because the only cross-repo blockers at the time were still open. ## Why it matters beyond one label The backlog sweep in `docs/UNSUPERVISED.md` keys on `Status/Blocked` and on the dependency graph. A permanently-labelled issue is one that never comes back as workable — the exact failure bit called out on 2026-09-07: *"they were unblocked/etc. and you should have started working on them"*. weblib-archive#30 is the largest item on the board and it would have stayed invisible. Note the cross-repo case is not an edge case here: weblib-archive#30 → cfbypass#1, #29 → weblib-fs#17, #57 → #30. The dependency graph in this org is mostly cross-repo. ## Adjacent Whatever the fix, it wants a test, and #9 is that nothing runs this repo's tests at all. The reporting side of the same problem: a job that does nothing still reports success, which is how this survived four green runs.
claude added the
Priority
High
2
Type
Bug
labels 2026-09-08 08:34:35 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: weblib/weblib-ci#11