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