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 # **Inside a container the act runner selects `sh`, not bash**, so # `set -o pipefail` fails the job on its second line with "Illegal # option" and the step never reaches the API. `homebrew-formula.yml` # carries the same `set -euo pipefail` without trouble because it # runs with **no container**, on the host image where bash is the # default — so "another workflow does it" is not evidence here. shell: bash 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 # `ca-certificates` is named because `--no-install-recommends` # skips it, and `ubuntu:24.04` ships no CA bundle of its own — # so curl comes up unable to verify TLS against our own Gitea # and fails with "error setting certificate file" (exit 77). # Every other containerised workflow here spells it out for the # same reason; this one did not, and cost a release cycle. apt-get update -qq apt-get install -y -qq --no-install-recommends \ ca-certificates 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. # The body is captured, not discarded, so a refusal is # diagnosable from this log alone. Whether the automatic # token carries issue-write scope is still unproven, and # "DELETE returned 403" without Gitea's own sentence costs # another merge to find out which of the two it is. body=$(mktemp) code=$( curl -sS -o "$body" -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 cat "$body" >&2 exit 1 ;; esac