From 0c944f238212eeabf1a75e9c640c6cc70a911f90 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Tue, 18 Aug 2026 17:36:05 -0400 Subject: [PATCH] ci: drop the claim label when an issue closes A `Closes #N` footer closes the issue on merge and leaves `Status/In Progress` on it, because Gitea's auto-close touches state and nothing else. #100 was closed and simultaneously marked as being actively worked on. `scripts/issue.sh close` does drop the label, and is exactly the call the footer exists to avoid making. This hooks the close rather than the merge. Stripping the label in the PR would work and would be a per-PR habit, which is what the footer removed in the first place; `issues: [closed]` covers the footer, issue.sh close and a click in the web UI alike, and asks nothing of anyone at any of them. Reopening deliberately does not restore the label: reopening says the work was not finished, not that somebody is at a keyboard now. Two costs, both stated in the file rather than discovered later. The runner has capacity 1 and is shared with an index build that can hold it for three hours, so this is not instant -- stale for an afternoon beats stale forever, which is what it was. And it is an eighth workflow, so CLAUDE.md's count moves with it. The audit stays, because a workflow that silently stops firing is the failure mode this area has already produced once: ./scripts/issue.sh list --state closed --label "Status/In Progress" Closes #102 --- .gitea/workflows/unclaim.yml | 86 ++++++++++++++++++++++++++++++++++++ CLAUDE.md | 19 ++++++-- 2 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 .gitea/workflows/unclaim.yml diff --git a/.gitea/workflows/unclaim.yml b/.gitea/workflows/unclaim.yml new file mode 100644 index 0000000..ea29eef --- /dev/null +++ b/.gitea/workflows/unclaim.yml @@ -0,0 +1,86 @@ +name: Unclaim + +# A `Closes #N` footer in a commit body closes the issue on merge — and +# leaves `Status/In Progress` on it, because Gitea's auto-close touches +# state and nothing else. So #100 was closed and simultaneously marked +# as being actively worked on, and `scripts/issue.sh close` (which does +# drop the label) is exactly the thing the footer exists to avoid +# calling. +# +# **This hooks the close, not the merge.** Stripping the label in the +# PR would work and would be a per-PR habit; habits are what the footer +# removed. `issues: [closed]` covers every path an issue can close by — +# the footer on merge, `issue.sh close`, someone clicking Close in the +# web UI — and asks nothing of anyone at any of them. +# +# **Reopening deliberately does not restore it.** Reopening says the +# work was not finished, not that somebody is at a keyboard doing it +# now; the claim gets re-made by whoever picks it up. +# +# **This is not instant, and should not be described as it.** The +# runner has capacity 1 and is shared with an index build that can hold +# it for three hours, so a label tweak can queue behind one. Stale for +# an afternoon beats stale forever, which is what it was. +# +# The audit that answers "is this still firing" stays in CLAUDE.md and +# is one command: +# +# ./scripts/issue.sh list --state closed --label "Status/In Progress" +# +# A workflow that silently stops working is the failure mode this whole +# area has already produced once. + +on: + issues: + types: [closed] + +jobs: + unclaim: + runs-on: ubuntu-latest + container: + image: ubuntu:24.04 + + steps: + - name: Drop the claim label + env: + # The automatic Actions token, as release.yml uses for the + # floor tag. It needs no more than write access to this repo. + TOKEN: ${{ secrets.GITEA_TOKEN }} + API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} + ISSUE: ${{ github.event.issue.number }} + run: | + set -euo pipefail + + apt-get update -qq + apt-get install -y -qq --no-install-recommends curl jq >/dev/null + + label_id=$( + curl -sSf -H "Authorization: token $TOKEN" "$API/labels?limit=100" | + jq -r '.[] | select(.name == "Status/In Progress") | .id' + ) + + # The label not existing is a repo somebody reorganised, not a + # failure of this run — say so and stop, rather than failing a + # job on every close from then on. + if [ -z "$label_id" ]; then + echo "unclaim: no 'Status/In Progress' label in this repo; nothing to do" + exit 0 + fi + + # DELETE is idempotent here: an issue that never carried the + # label answers the same as one that did, which is what makes + # this safe to run on *every* close rather than only the ones + # that were claimed. + code=$( + curl -sS -o /dev/null -w '%{http_code}' -X DELETE \ + -H "Authorization: token $TOKEN" \ + "$API/issues/$ISSUE/labels/$label_id" + ) + + case "$code" in + 204) echo "unclaim: #$ISSUE is closed and unclaimed" ;; + *) + echo "unclaim: DELETE returned $code for #$ISSUE" >&2 + exit 1 + ;; + esac diff --git a/CLAUDE.md b/CLAUDE.md index 58ce0b9..ee56427 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,8 +85,18 @@ release decision. The rule that the issue number stays out of the still drops the footer. `./scripts/issue.sh list --state open` after a merge, looking for what you just shipped; `./scripts/issue.sh close ` for whatever did not take, with a comment naming the commit. -`close` also drops `Status/In Progress`, because a claim outlives the -work if nothing takes the label off. + +**Unclaiming is automatic, and it is hooked to the close rather than +to the merge.** Gitea's auto-close changes state and nothing else, so a +footer left `Status/In Progress` on a closed issue — #100 was closed +and marked as being actively worked on at the same time. +`.gitea/workflows/unclaim.yml` runs on `issues: [closed]`, which covers +the footer, `issue.sh close` and a click in the web UI alike; stripping +the label in the PR instead would have been a per-PR habit, and habits +are what the footer removed. It is not instant — the runner has +capacity 1 — and reopening deliberately does not restore the label. +`./scripts/issue.sh list --state closed --label "Status/In Progress"` +is how you find out it has stopped firing. ## Planning @@ -2176,10 +2186,11 @@ Pre-commit runs vet, lint, codegen check, and frontend typecheck in parallel. Pr ## CI -Seven workflows in `.gitea/workflows/`. Five of them package and +Eight workflows in `.gitea/workflows/`. Five of them package and publish (`arch-package`, `homebrew-formula`, `index-artifact`, `android-apk`, `desktop-assets`); `release.yml` decides *whether* four of -those run at all; only `ci.yml` gates, and it is the one to look at when +those run at all; `unclaim.yml` is housekeeping on the tracker and +touches no code; only `ci.yml` gates, and it is the one to look at when deciding whether a push was healthy. **`release.yml` is the entry point for all of it.** On every push to