feat: close issues through a script
Closing was the last regular tracker operation with no script behind it. The only way to move state: was a raw `tea api -X PATCH` against repos/OWNER/REPO/issues/N with a hand-written body, which spells out the owner, the repo and the request shape — the three things _gitea.py exists to hide — and which needs a Bash(tea api *) permission wide enough to cover -X DELETE on the repository. close.py takes explicit ids, one or many, as a local slug or as any key form pull.py accepts (42, #42, owner/repo#42, a URL). A slug resolves through its gitea: field while the file is there and through .remote.json after push has dropped it, so an issue with no local copy is still closeable by name. --reopen is the same run backwards. State only: the payload carries state and nothing else. Closing is not an edit; editing stays pull -> change -> push --update. No --milestone and no --label either — which issues are finished is a judgement about content, and this only carries one out, one named id at a time. An origin: local issue is refused: it is not in the tracker, so there is no state there to change, and the error names the id rather than quietly editing one field of a local file. Every argument is resolved before anything is sent, so a typo in the third id cannot leave the first two closed, and one run addresses one repo — a key that names its own is sent there instead of to whatever repo the CWD happens to be in. The local file is written only after the tracker confirmed this write: an object carrying the number that was PATCHed, in the state that was asked for (close.confirmed). A non-2xx, a transport that would not run, an answer for another issue, a 200 that still says open — the run stops and the file is byte for byte what it was. --dry-run prints the same lines, makes no request at all and needs no pinned login. tea-runner rule 4 narrows accordingly: closing was forbidden because nothing but a raw call could do it, not because it is dangerous. It may now close the ids the caller named, and no others; deleting and retitling stay forbidden. tests/test_close.py stubs the transport at _gitea.api and, for the non-2xx path, one layer lower at _gitea.subprocess so a CLI that exits 1 is proved end to end. 286 tests, no network, no tmp/issues. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+50
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: sync
|
||||
description: Move issues between the local store and Gitea — pull issues into tmp/issues/, push local issues up, post comments. Load when the user asks to fetch/read a Gitea issue, publish an issue, list what exists in the tracker, or comment on one. Working with an issue's content (writing, grepping, validating, dependency graph) is /tea:issue and needs no network.
|
||||
description: Move issues between the local store and Gitea — pull issues into tmp/issues/, push local issues up, post comments, close and reopen them. Load when the user asks to fetch/read a Gitea issue, publish an issue, list what exists in the tracker, comment on one, or close/reopen one. Working with an issue's content (writing, grepping, validating, dependency graph) is /tea:issue and needs no network.
|
||||
---
|
||||
|
||||
# /tea:sync — the bridge between the local store and Gitea
|
||||
@@ -41,6 +41,7 @@ the `tea-guard` hook reads. No pin → exit with a pointer to `/tea:auth`.
|
||||
| `pull.py <key…>` or `pull.py --milestone M \| --label L \| -q TEXT` | Gitea → `tmp/issues/<id>.md`, plus `<id>.comments.md` when the thread is not empty |
|
||||
| `push.py [id…] [--update] [--dry-run]` | local → Gitea; validates first, **deletes the local file on success** and prints where it lives now |
|
||||
| `comment.py <id> --file F \| --body TEXT [--edit N]` | post or edit a comment, then refetch the thread |
|
||||
| `close.py <id…> [--reopen] [--dry-run]` | set `state` in Gitea and in the local copy with it; explicit ids only, no bulk filter |
|
||||
| `labels.py [--dry-run] [--fix]` | bootstrap the canonical `type/*` + `severity/*` set in a repo; exact names left alone, lookalikes reported, drift fixed only with `--fix` |
|
||||
| `map.py`, `_gitea.py` | the two layers the commands import — not commands |
|
||||
|
||||
@@ -308,6 +309,54 @@ a git repo no `ref` is sent and a warning names the issues that went up without
|
||||
one. Reading the branch is the only thing these scripts ask git for — they
|
||||
never check out, create, or write anything.
|
||||
|
||||
## Closing and reopening
|
||||
|
||||
```bash
|
||||
python3 <skill-base-dir>/scripts/close.py wire-sqlc-appclick # by slug
|
||||
python3 <skill-base-dir>/scripts/close.py 42 '#43' # by number
|
||||
python3 <skill-base-dir>/scripts/close.py --reopen 42
|
||||
python3 <skill-base-dir>/scripts/close.py --dry-run 42 43 # no request at all
|
||||
```
|
||||
|
||||
`close.py` is the only supported way to move `state:`. Never hand-roll
|
||||
`tea api -X PATCH -d '{"state":"closed"}' repos/OWNER/REPO/issues/N`: it spells
|
||||
out the owner, the repo and the request body — the three things this layer
|
||||
exists to hide — and it needs a `Bash(tea api *)` permission that also covers
|
||||
`-X DELETE` on the repository.
|
||||
|
||||
**State only.** The payload is `{"state": …}` and nothing else — no title, no
|
||||
body, no labels, no milestone. Closing is not an edit; editing is `pull.py` →
|
||||
change → `push.py --update`.
|
||||
|
||||
**Explicit ids only.** There is no `--milestone` and no `--label`: which issues
|
||||
are finished is a judgement about content, and this script only carries one
|
||||
out, one named id at a time. Deleting an issue is out of scope too — Gitea can,
|
||||
and it is not an operation of this workflow.
|
||||
|
||||
What may be named, and what happens to the local copy:
|
||||
|
||||
| named | resolved through | local file |
|
||||
|---|---|---|
|
||||
| a slug with a file on disk | its `gitea:` field | `state:` rewritten, `synced:` refreshed |
|
||||
| a slug whose file push dropped | `.remote.json` | none to write — say so and move on |
|
||||
| `42`, `#42`, `owner/repo#42`, a URL | the key itself; the ledger supplies the slug | rewritten when a file of that slug is there |
|
||||
| a slug with `origin: local` | — | **refused**: it is not in the tracker, and the error names the id |
|
||||
|
||||
The local file is written only after the tracker has confirmed *this* write: an
|
||||
object carrying the very number that was PATCHed, in the state that was asked
|
||||
for. A non-2xx, a `tea` that would not run, an answer for another issue, a 200
|
||||
that still says `open` — the run stops and the file is byte for byte what it
|
||||
was. `--dry-run` prints the same lines and makes no request at all, so it needs
|
||||
no pinned login.
|
||||
|
||||
Gitea refuses to close an issue that its own dependency graph still blocks. The
|
||||
refusal arrives as a non-2xx with the tracker's own words: close the blockers
|
||||
first, or unlink them in the web UI.
|
||||
|
||||
The index is rebuilt when at least one local file changed, so `INDEX.md` never
|
||||
outlives the state it reports. Nothing is deleted here — unlike a push, a close
|
||||
leaves the working copy where it is.
|
||||
|
||||
## What crosses the boundary, and what does not
|
||||
|
||||
| domain | Gitea | note |
|
||||
|
||||
Reference in New Issue
Block a user