Mitigate the stuck/reasserted cross-repo Status/Blocked (does not fully fix #11) #13
Labels
Clear labels
Compat
Breaking
Breaking change that won't be backward compatible
Kind
Documentation
Documentation changes
Kind
Enhancement
Improve existing functionality
Kind
Feature
New functionality
Kind
Question
A question that has to be answered before the work can be specified
Kind
Security
This is security issue
Kind
Testing
Issue or pull request related to testing
Priority
Critical
1
The priority is critical
Priority
High
2
The priority is high
Priority
Medium
3
The priority is medium
Priority
Low
4
The priority is low
Reviewed
Confirmed
1
Issue has been confirmed
Reviewed
Duplicate
2
This issue or pull request already exists
Reviewed
Invalid
3
Invalid issue
Reviewed
Won't Fix
4
This issue won't be fixed
Status
Blocked
1
Something is blocking this issue or pull request
Status
Need More Info
2
Feedback is required to reproduce issue or to continue work
Status
On Hold
3
Deliberately deferred. Not blocked by a dependency and not waiting on a fact
Status
Abandoned
4
Somebody has started to work on this but abandoned work
Type
Bug
Something is not working
Type
Chore
Infrastructure, CI, tooling or packaging. No user-visible change
Type
Epic
An umbrella issue with sub-issues. Never worked directly
Type
Meta
About how work is done here rather than about the code
Type
Spike
The deliverable is a measurement or a decision, not merged code
Type
Task
An ordinary unit of work with a deliverable. The default
No labels
Milestone
No items
No Milestone
Notifications
Due Date
No due date set.
Reference: weblib/weblib-ci#13
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Relates to weblib-ci#11 -- deliberately not
Closes, see "What thisactually is" below: this does not fix the title's claim
("Reconciler can never clear Status/Blocked when every blocker is in another
repo") -- that is still true after this merges. It fixes the worse half: the
reconciler no longer lies about it.
What this actually is
Before: a cross-repo
Status/Blockedwas stuck, silently. Worse, foundlive on 2026-09-09: it was being reasserted over a human's hand-removal
every 15 minutes, also silently. Nothing in the log said why, or that it was
happening at all.
After: the label is still stuck in exactly the same cases (the token
genuinely cannot resolve the blocker -- see below), but now the run says so
by issue number every time, and it stops fighting a human's correction. That
is "loudly unknown" replacing "silently wrong" -- a real improvement, worth
merging on its own, but not the fix the issue title describes. Do not close
#11 on merge. Leave it open, pointing at the decision below, which is what
would actually let a cross-repo state be positively confirmed.
Decision needed (on #11, not blocking this PR)
Should a broader-scoped credential be added so cross-repo blocker state can
be positively confirmed, instead of only ever reported as unknown?
That is bit's call -- a new credential has a real blast radius, so I have not
added one unilaterally. Evidence for the decision is below.
What this changes for you
Once merged, a cross-repo
Status/Blockedwill stop being silently stuck orsilently reasserted -- the reconciler logs
unknown <repo>#<n>: N blocker(s) unresolvable with this token -- leaving 'Status/Blocked' as-is (present|absent)and leaves the label exactly where it was. A human still hasto look and decide; this only stops the automation from fighting that
decision.
Evidence: two credentials, same instant, opposite results
This is what rules out "the endpoint is broken" -- it is a permission
substitution, not a general limitation. Same call
(
GET repos/weblib/weblib-archive/issues/<n>/dependencies), same issues, samemoment, only the token differs.
Under a personal token with read access to the whole org (not the
reconciler's token -- a human's), weblib-archive#30 and #29's dependencies
come back fully populated:
Under weblib-archive's own scoped Actions token -- the credential the
reconciler actually runs with -- the identical call for #30 returns two
entries that are Go's zero-value
Issuestruct instead, verbatim from thelive run:
Entries are never omitted (
len(deps)is 2, matching the two realblockers) -- each one is a placeholder with
id: 0,state: "",repository: null, and every other field blank or zero.d.get("state") != "closed"on the old code read that blank as "still open", forever.The wall is hard, not a permissions nuance. Same scoped token, direct
GETinstead of via/dependencies:The token cannot see
cfbypassat all -- not "cannot see this issue", cannotsee the repository. Making cross-repo state resolvable needs a token that can
read across repos; that is the decision above.
(All three snippets above are one measurement each, by me, not yet
independently re-run by anyone else -- flagging that for the cold re-read
rather than presenting single live-instance output as settled fact.)
The fix
is_resolved()keys onid == 0, not on the emptystate--idcannotlegitimately be zero for a real issue, and a future Gitea populating
statewhile still stubbing the rest of the entry would silently revive the bug if
the check were on
stateinstead.reconcile()is now a three-way split instead of two:own, even alongside unresolvable entries -- this direction must never
soften)
dependencies always resolve fully, so this path is untouched in practice --
TestSameRepoUnchangedpins that so a cross-repo fix cannot regress it)change nothing, log the issue number
This keeps the asymmetry from cfbypass#19 (a false "unblocked" sends someone
at work that cannot be done; a false "blocked" only hides work) -- it adds a
third outcome for the case the old two-way split had no honest way to
express, rather than inverting either existing one.
Acceptance test, run for real
Not the throwaway -- the actual weblib-archive#30, #57, #54, #29, which
carried
Status/Blockedgoing in for four different reasons (checked with apersonal token beforehand): #30 both blockers closed-but-cross-repo, #29 one
same-repo-closed set plus one cross-repo open (weblib-fs#17), #57 and #54
same-repo blockers that are genuinely still open.
Ran this fix branch for real against
weblib/weblib-archive, under its ownActions token (a throwaway branch off
developmentthat only swapped thecomposite action's ref,
workflow_dispatched, then deleted). Firstnon-dry-run, to prove no label moved; then
--dry-runagain to capture thelog without depending on the (broken, 500s-for-every-id) job-log viewer:
unknown, not silently skipped and notwrongly reported as resolved. #29 matters here specifically because its
cross-repo blocker (weblib-fs#17) happens to still be genuinely open under a
personal token -- a label that is correct by luck is not evidence the
reconciler resolved it, and this confirms it did not: it is unknown to this
token exactly like #30 is.
resolved normally, were found genuinely open, and the label was already
correct, so nothing was logged (matches
TestSameRepoUnchanged's "alreadycorrect is a no-op" case). Confirmed directly: their
Status/Blockedlabelswere unchanged by the earlier non-dry-run pass.
verified by reading each issue's labels via the API before and after, not
by trusting the run's exit code.
Nothing on #30/#57/#54/#29 was hand-edited at any point; every state change
(none, in this case) came only from the reconciler's own calls.
Tests
test_sync_blocked_label.py, new, 12 tests, offline against a localhttp.serverfake (same pattern astest_report_job_log.py):Proved by fault injection. Stashed the fix (kept the new tests), leaving
sync_blocked_label.pyat its pre-#11-fix state:test_never_reasserts_a_label_a_human_removedreproduced the live bugverbatim against the old code:
changed == [(30, 'add', 'blocked by #0, #0')]-- it really does read the two zero-value stubs' blank
#0as things toreport as blockers and put the label back on. Every other test (same-repo,
hands-off, no-deps, no-label-in-repo, dry-run) still passed, so the fault was
isolated to exactly the new behaviour. Restored the fix -- diffed
byte-identical against the pre-revert copy -- and the suite is green again.
Not done here, on purpose
is closed with its dependency and label removed. Its comment history still
has the original probe output (superseded by the quotes above, which are
copied verbatim from it so the evidence does not depend on a throwaway
issue staying around).
mainin both weblib-archive(weblib-archive#70) and cfbypass (cfbypass#35) still runs each repo's own
pre-migration local copy of this script, which can no longer find
Status/Blockedat all since labels moved to the org(
repos/<repo>/labelsnow returns[]) -- every scheduled run onmaincurrently prints
skipped: no 'Status/Blocked' label in this repoand doesnothing, for any issue, in either repo.
developmentin weblib-archivealready migrated to consuming this repo via the composite action
(weblib-archive#44);
mainhas not been promoted since. This is live andcurrently worse than #11 -- both filed and assigned to bit, since promoting
mainis each repo's release process to decide, not something to fold intothis script fix.
Treat an unresolvable cross-repo blocker as unknown, not as open foreverto Mitigate the stuck/reasserted cross-repo Status/Blocked (does not fully fix #11)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>