Name the cause when a watch stops on an auth failure
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

A 401/403 was handled as one thing, so watchpr re-minted on every
rejection and reported "token expired" for a permission boundary or an
anonymous run that had no token to expire, sending the reader after the
wrong problem.

Classify a 401/403 as a rejected credential, a permission denial, or a
request that carried no token, and re-mint only the first.
Reject a re-minted empty token instead of replaying anonymously.
Report the classified cause from --once as well as from the watch loop.
Document watchpr's exit behaviour per cause.
This commit is contained in:
2026-09-19 16:13:38 +10:00
parent 72adebbf8b
commit 90ce747a61
6 changed files with 387 additions and 17 deletions
+15 -4
View File
@@ -175,11 +175,22 @@ wrapped per stage (login / read denied / write denied) via `ErrVaultDenied`.
## Gotchas
- `watchpr` exits 0 with no output changes on `--once` (just prints state).
- Gitea tokens expire in ~1h, shorter than a watch: the client re-mints once on a
401/403 and replays the request. If the fresh token is rejected too, `watchpr`
exits non-zero rather than polling blind.
- Gitea tokens expire in ~1h, shorter than a watch: the client re-mints once when
the credential it sent was rejected and replays the request. If the fresh token
is rejected too, `watchpr` exits non-zero rather than polling blind.
- A 401/403 is classified before anything is re-minted, because only one of the
three cases is a stale token: `ErrNoCredential` (the request carried no token —
anonymous access to something not public), `IsPermissionDenied` (a 403 whose
body names no credential, so the identity is simply not allowed) and
`IsCredentialRejected` (any 401, or a 403 mentioning a token/scope/sign-in,
which is what Gitea returns for a token missing a scope). Only the last
re-mints; the others abort immediately, since a fresh token cannot fix them and
blaming one hides the real cause. `IsAuthError` stays "any 401/403" — all three
end a watch.
- `watchpr` polls anonymously when no token can be minted (public repos work
fine); only a real 401/403 reaches for Vault.
fine); an anonymous run never reaches for Vault, on any status code.
- A re-mint that hands back an empty token is an error: replaying with it would
drop the Authorization header and silently continue as an anonymous watcher.
- The token cache is process-wide (mutex-guarded); `RefreshGiteaToken` replaces
it. Tests call the unexported `fetchGiteaToken` to avoid the cache.
- `agentvault` never puts a secret in an error string: Vault decode failures and