5.8 KiB
name, description
| name | description |
|---|---|
| gitea-api-pr-operations | Open, list, merge, and verify pull requests (and diagnose issue-API scope failures) on the Dankclaw Gitea via its REST API from a workstation, keeping the token out of argv (curl -K config file, mode 600) — includes the credential-store extraction one-liner, the merge endpoint's Do-field law and Content-Type-header trap, the suggested-URL-is-not-a-PR gotcha, and the insufficient-token-scope trap. |
Gitea API PR operations (Dankclaw)
git push on this fleet prints Create a new pull request for ... — that is a
suggestion, not a PR. Verify (GET /pulls?state=open → expect []) before
claiming a PR "should exist"; create it via the API when asked.
Endpoint + auth
- API base:
http://192.168.0.143:3000/api/v1/repos/dank/<repo> - Token lives in the git credential store configured by
git config --get credential.helper→store --file ~/.config/secrets/gitea, line formathttp://user:TOKEN@192.168.0.143:3000. - The repo-local
.envGITEA_TOKEN(main checkout, gitignored) carriesread:issuetoo (measured 2026-10-18): filing issues needs THAT token, the credential-store one 403s on issues.
Extract the token (the http:// scheme contains a colon — scheme-stripping
awk regexes silently grab the port instead; use this anchored sed):
tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)
[ -n "$tok" ] && echo "token-extracted(len=${#tok})" # length check, never echo the value
Keep the token out of argv (repo secret law)
curl -H "Authorization: token $tok" is world-readable in /proc/<pid>/cmdline.
Use a mode-600 config file:
cfg=$(mktemp); chmod 600 "$cfg"
printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
- POST JSON needs a second header line:
header = "Content-Type: application/json". There IS NOcontent-typeconfig option — curl rejects the file. rm -f "$cfg"when done (a mode-600 token file left in /tmp is its own leak).
Operations
# list open PRs
curl -sS -K "$cfg" ".../pulls?state=open" | jq -r '.[] | [.number,.head.name,.base.name,.title] | @tsv'
# create (build the payload as a FILE, see below)
curl -sS -K "$cfg" -o /tmp/pr.json -w '%{http_code}\n' -X POST -d @/tmp/pr-payload.json ".../pulls"
# verify: 201 is not enough — check mergeable
curl -sS -K "$cfg" ".../pulls/1" | jq '{number,state,merged,mergeable,base:.base.ref,head:.head.ref,url:.html_url}'
Merging: the field is Do, and it is a STRING naming the strategy
Measured 2026-10-18 on Gitea at 192.168.0.143:3000 (POST .../pulls/<n>/merge).
MergePullRequestOption is {From, To, When, Do} — there is no DoMerge and
no merge_method. Every wrong shape is a 422 with a distinct message, so read it:
| body sent | http | message |
|---|---|---|
{"DoMerge":true,"merge_method":"merge"} |
422 | [Do]: Required |
{"Do":true,"merge_method":"merge"} |
422 | []: json: cannot unmarshal JSON boolean into Go string within "/Do" |
{"Do":"true"} |
422 | [Do]: In… (validation: not a strategy name) |
form body Do=true&merge_method=merge |
422 | [Do]: Required (the value must be a strategy name) |
{"Do":"merge"} |
200 | merged |
curl -sS -K "$cfg" -H 'Content-Type: application/json' -X POST -d '{"Do":"merge"}' ".../pulls/4/merge"
# then prove it: .merged true, .state closed, .merge_commit_sha set, and origin/main moved
curl -sS ".../pulls/4" | jq -c '{number,state,merged,merge_commit_sha}'
Strategy names: merge, rebase, rebase-merge, squash, manually — a
strategy the repo does not allow is rejected too.
Merge trap (measured 2026-10-18): on POST /merge the Content-Type header
must go on the command line (-H 'Content-Type: application/json'). Same
curl that succeeds on POST /pulls with Content-Type supplied only via the -K
config file returns 405 with an EMPTY body on /merge. A 405 there is this
trap (or a base-moved mergeability recheck — GET the PR, see mergeable:true,
then retry with -H), NOT a permission problem.
Building issue/PR bodies without quote carnage
Hand-written jq -n --arg t "..." '{title:$t, body:"..."}' one-liners with long
markdown bodies WILL break on a stray quote (and the broken quoting can swallow
the following commands). Build bodies as files (heredoc or write tool), then:
b=$(jq -Rs . body.md)
jq -n --arg t "$title" --argjson b "$b" '{title:$t, body:$b, head:"<branch>", base:"main"}' > payload.json
curl ... -d @payload.json
Gotchas measured 2026-10-14 / 2026-10-17 / 2026-10-18
- Unauthenticated/bad token → HTTP 401 body
{"message":"not found"};jq '.[]|[.number,..]'then fails with cannot index "not found" — that is an auth failure, not a missing repo. - Insufficient token scope ≠ missing data. The credential-store token
(measured 2026-10-17) carries only
write:repository,write:user; the issues API answers HTTP 403{"message":"token does not have at least one of required scope(s), required=[read:issue], token scope=write:repository,write:user"}. jq surfaces that JSON as the cannot index error — do NOT conclude the issue list is empty, and note POSTs under this token create NOTHING (verify, don't assume retry safety). Filing issues needs a fresh token minted in the Gitea web UI withread:issue+write:issue(Frank-class credential action; ask) — OR use the main-checkout.envGITEA_TOKEN, which already has issue scope. PR endpoints work fine with either token (pullsPOST/GET/merge all 200/201). - On the created-PR response
.head.namecan be null; verify with theGET /pulls/<n>and use.head.ref. - Swagger JSON is disabled on this instance —
/api/swagger.v1.json,/api/swagger.jsonand/api/v1/swagger.jsonall 404. Do not go looking for the schema there; the 422 messages name the field and its Go type, which is the faster instrument.