Resolve labels from the org, and never touch a held issue #4
@@ -45,6 +45,18 @@ import urllib.request
|
||||
DEFAULT_LABEL = "Status/Blocked"
|
||||
DEFAULT_HOST = "https://git.chaosbit.de"
|
||||
|
||||
#: Statuses that mean a human has decided something about this issue which
|
||||
#: outranks the dependency graph. bit, 2026-09-07: *"the reconciler must not
|
||||
#: touch issues that are already on hold or abandoned"*.
|
||||
#:
|
||||
#: "Not touch" is literal - neither add nor remove. Adding would be actively
|
||||
#: destructive once `Status/*` is exclusive again, because the add would
|
||||
#: *replace* the human's label rather than sit beside it, and `Status/On Hold`
|
||||
#: is precisely what makes the backlog sweep skip an issue. A parked issue
|
||||
#: would silently become an available one, every fifteen minutes, with nothing
|
||||
#: in the log to say so.
|
||||
HANDS_OFF = ("Status/On Hold", "Status/Abandoned")
|
||||
|
||||
|
||||
class Forge:
|
||||
def __init__(self, base, token):
|
||||
@@ -75,15 +87,29 @@ class Forge:
|
||||
|
||||
|
||||
def label_id(forge, repo, name):
|
||||
"""The label's id, or None if this repo has no such label.
|
||||
"""The label's id, or None if neither the repo nor its org has one.
|
||||
|
||||
Repo first, then the organisation. Labels moved to the org on 2026-09-07
|
||||
(weblib-archive#63) and `repos/<r>/labels` now returns `[]` in all five
|
||||
repos, which made this return None everywhere and turned the whole script
|
||||
into a silent no-op - every run printed "skipped" and reported success.
|
||||
Checking both means it does not care how a given instance is arranged.
|
||||
|
||||
Returning None rather than exiting matters when several repos are passed:
|
||||
aborting on the third would leave the first two already modified, which is
|
||||
a worse state than doing nothing. weblib-viewer has no labels at all.
|
||||
a worse state than doing nothing.
|
||||
"""
|
||||
for label in forge.get(f"repos/{repo}/labels?limit=100"):
|
||||
if label["name"] == name:
|
||||
return label["id"]
|
||||
owner = repo.split("/")[0]
|
||||
for path in (f"repos/{repo}/labels?limit=100",
|
||||
f"orgs/{owner}/labels?limit=100"):
|
||||
try:
|
||||
labels = forge.get(path) or []
|
||||
except urllib.error.HTTPError:
|
||||
# A user-owned repo has no org endpoint; not an error worth dying on.
|
||||
continue
|
||||
for label in labels:
|
||||
if label["name"] == name:
|
||||
return label["id"]
|
||||
return None
|
||||
|
||||
|
||||
@@ -110,6 +136,15 @@ def reconcile(forge, repo, label_name, dry_run):
|
||||
changed = []
|
||||
for item in open_items(forge, repo):
|
||||
number = item["number"]
|
||||
names = {l["name"] for l in item.get("labels") or []}
|
||||
# A human has already ruled on this one. Leave it entirely alone -
|
||||
# neither add nor remove - rather than letting the dependency graph
|
||||
# overwrite a deliberate decision. See HANDS_OFF.
|
||||
held = names.intersection(HANDS_OFF)
|
||||
if held:
|
||||
print(f" hands off {repo}#{number} ({', '.join(sorted(held))})",
|
||||
flush=True)
|
||||
continue
|
||||
deps = forge.get(f"repos/{repo}/issues/{number}/dependencies") or []
|
||||
if not deps:
|
||||
# No edges, no opinion. See the docstring.
|
||||
|
||||
Reference in New Issue
Block a user