gitea-image-prune.sh: auto-discover credentials from git

When GITEA_TOKEN/--token-file aren't set and the script runs inside a
Gitea-host clone, reuse git's stored credential: the token embedded in
the remote URL, else a configured credential helper (git credential fill,
no prompting). Switch to HTTP Basic auth (user:token) when a username is
known so both API tokens and the embedded-URL/helper credential work;
keep the token header for a bare GITEA_TOKEN. Banner reports the source.
Live-verified: env token + git credential helper (as kisfenyo) both list
and resolve OCI sizes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 09:28:55 +02:00
parent 761dc3856e
commit 5b4d8ec6cf
3 changed files with 83 additions and 8 deletions
+13
View File
@@ -2,6 +2,19 @@
All notable changes to the operator helper scripts. Newest on top.
## 2026-06-17 (later)
### Changed — `gitea-image-prune.sh`
- Credential auto-discovery: when `GITEA_TOKEN`/`--token-file` are not set and the
script runs inside a Gitea-host clone, it reuses git's stored credential — the
token embedded in the remote URL, else a configured credential helper
(`git credential fill`, never prompting). Lets you run it from a configured
clone with no token. Startup banner reports the credential source + user.
- Auth now uses HTTP Basic (`user:token`) when a username is known (so both API
tokens and the embedded-URL/helper credential work), falling back to the
`Authorization: token` header for a bare `GITEA_TOKEN`. Live-verified both paths
(env token; git credential helper as `kisfenyo`).
## 2026-06-17
### Added — `gitea-image-prune.sh`
+17 -1
View File
@@ -47,7 +47,23 @@ Confirmed live on Gitea 1.26.2 (2026-06-17), the reclaim path is **three steps**
> accumulated orphan manifests on `felhom-hub` then freed **86 MiB**. After
> reclaim, surviving tags (`latest`, `0.1.3`, …) still `docker pull` cleanly.
### Token & required scopes
### Credentials
The script resolves credentials in this order:
1. `GITEA_TOKEN` env var
2. `--token-file <path>`
3. **git's stored credential for the Gitea host** — auto-discovered when you run
the script inside a clone: first the token embedded in the remote URL
(`https://user:token@host/…`), else a configured **credential helper**
(`git credential fill`). No prompting.
So from a configured clone you can just run `./gitea-image-prune.sh --repo … list`
with no token at all. The startup banner prints which source was used. Caveat: a
*git* credential may only carry repo scope — if it lacks package/admin scope, the
call returns a 403 naming the missing scope (see below).
### Required scopes
Pass an admin-user token via `GITEA_TOKEN` (env) or `--token-file <path>`. The
token must belong to a Gitea **site-admin** user. Minimal fine-grained scopes
+53 -7
View File
@@ -42,6 +42,12 @@
# and their blobs are freed by the daily "@midnight" run (or on the next Gitea
# restart — RUN_AT_START was enabled in app.ini on 2026-06-17).
#
# CREDENTIALS (in order: GITEA_TOKEN env -> --token-file -> git's stored
# credential for the Gitea host: the remote URL's embedded token, else a
# configured credential helper). Auto-discovery lets you just run the script
# inside a clone with no token fuss — but a *git* credential may only have repo
# scope; if so, a 403 will name the missing package/admin scope.
#
# REQUIRED TOKEN (env GITEA_TOKEN, or --token-file). Must belong to a Gitea
# site-admin user. Minimal fine-grained scopes (Gitea 1.26):
# read:package — list packages / versions / files (list, prune planning)
@@ -149,16 +155,55 @@ for bin in curl jq; do
done
# Token: --token-file beats env. Never printed.
GITEA_USER="${GITEA_USER:-}" # set when credentials come with a username (-> Basic auth)
CRED_SRC="GITEA_TOKEN env"
if [[ -n "$TOKEN_FILE" ]]; then
[[ -r "$TOKEN_FILE" ]] || { error "--token-file not readable: $TOKEN_FILE"; exit 1; }
GITEA_TOKEN="$(tr -d ' \t\r\n' < "$TOKEN_FILE")"
CRED_SRC="--token-file"
fi
# Auto-discover from git when no explicit token: reuse the credential git already
# has for the Gitea host (embedded remote URL, or a configured credential helper).
# Convenient, but a *git* token may lack package/admin scopes — a 403 will say so.
discover_git_credential() {
command -v git &>/dev/null || return 1
git rev-parse --is-inside-work-tree &>/dev/null || return 1
local host="${GITEA_URL#*://}"; host="${host%%/*}"
# (1) credentials embedded in the remote URL (https://user:token@host/...)
local url; url="$(git remote get-url origin 2>/dev/null || true)"
if [[ "$url" == *"$host"* && "$url" =~ ^https?://([^:/@]+):([^@/]+)@ ]]; then
GITEA_USER="${BASH_REMATCH[1]}"; GITEA_TOKEN="${BASH_REMATCH[2]}"; CRED_SRC="git remote URL"; return 0
fi
# (2) a configured credential helper (store / cache / manager). Never prompt.
if git config --get credential.helper &>/dev/null; then
local out user pass
out="$(printf 'protocol=https\nhost=%s\n\n' "$host" | GIT_TERMINAL_PROMPT=0 git credential fill 2>/dev/null || true)"
pass="$(printf '%s\n' "$out" | sed -n 's/^password=//p' | head -1)"
user="$(printf '%s\n' "$out" | sed -n 's/^username=//p' | head -1)"
if [[ -n "$pass" ]]; then GITEA_TOKEN="$pass"; GITEA_USER="${user:-$OWNER}"; CRED_SRC="git credential helper"; return 0; fi
fi
return 1
}
if [[ -z "$GITEA_TOKEN" ]]; then
error "No token. Set GITEA_TOKEN env or pass --token-file <path>."
error "Needs a site-admin token with read/write:package + read/write:admin."
discover_git_credential || true
fi
if [[ -z "$GITEA_TOKEN" ]]; then
error "No credentials. Set GITEA_TOKEN env, pass --token-file <path>, or run inside"
error "a clone of a ${GITEA_URL#*://} repo whose git credentials are configured."
error "Needs a site-admin token with read/write:package + read/write:admin scope."
exit 1
fi
# Auth method: Basic (user:token) when a username is known — works for both API
# tokens and passwords; bare 'token' header otherwise. OCI /v2/ always uses Basic.
declare -a AUTH
if [[ -n "$GITEA_USER" ]]; then AUTH=(-u "${GITEA_USER}:${GITEA_TOKEN}"); else AUTH=(-H "Authorization: token ${GITEA_TOKEN}"); fi
OCI_USER="${GITEA_USER:-$OWNER}"
# Default audit log
if [[ -z "$LOG_FILE" ]]; then
LOG_FILE="${SCRIPT_DIR}/logs/gitea-prune-$(date +%Y-%m-%d).log"
@@ -182,7 +227,7 @@ audit() { # free-form line -> audit log (token-free by construction)
api_get() {
local path="$1" out code
out="$(mktemp)"
code="$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
code="$(curl -sS "${AUTH[@]}" \
-o "$out" -w '%{http_code}' "${GITEA_URL}${path}" || true)"
if [[ ! "$code" =~ ^2[0-9][0-9]$ ]]; then
error "GET ${path} -> HTTP ${code}"
@@ -194,7 +239,7 @@ api_get() {
api_delete() {
local path="$1" code
code="$(curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
code="$(curl -sS -X DELETE "${AUTH[@]}" \
-o "$TMP/del.body" -w '%{http_code}' "${GITEA_URL}${path}" || true)"
echo "$code"
[[ "$code" == "204" ]]
@@ -202,7 +247,7 @@ api_delete() {
api_post() {
local path="$1" code
code="$(curl -sS -X POST -H "Authorization: token ${GITEA_TOKEN}" \
code="$(curl -sS -X POST "${AUTH[@]}" \
-o "$TMP/post.body" -w '%{http_code}' "${GITEA_URL}${path}" || true)"
if [[ ! "$code" =~ ^2[0-9][0-9]$ ]]; then
error "POST ${path} -> HTTP ${code}"
@@ -215,7 +260,7 @@ api_post() {
oci_get() {
local name="$1" ref="$2" out code
out="$(mktemp)"
code="$(curl -sS -u "${OWNER}:${GITEA_TOKEN}" \
code="$(curl -sS -u "${OCI_USER}:${GITEA_TOKEN}" \
-H 'Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json' \
-o "$out" -w '%{http_code}' "${GITEA_URL}/v2/${OWNER}/${name}/manifests/${ref}" || true)"
if [[ ! "$code" =~ ^2[0-9][0-9]$ ]]; then rm -f "$out"; return 1; fi
@@ -348,7 +393,7 @@ find_orphans() {
# echoes "ok <code>" on 2xx, "denied" on 403, "err <code>" otherwise; rc reflects.
trigger_cleanup_cron() {
local code
code="$(curl -sS -X POST -H "Authorization: token ${GITEA_TOKEN}" \
code="$(curl -sS -X POST "${AUTH[@]}" \
-o "$TMP/cron.body" -w '%{http_code}' \
"${GITEA_URL}/api/v1/admin/cron/${CLEANUP_CRON}" || true)"
if [[ "$code" =~ ^2[0-9][0-9]$ ]]; then echo "ok ${code}"; return 0; fi
@@ -617,6 +662,7 @@ info "╔═══════════════════════
info "║ Gitea container-image prune ║"
info "╚══════════════════════════════════════════╝"
info "Server: ${GITEA_URL} Owner: ${OWNER} Log: ${LOG_FILE}"
info "Auth: ${CRED_SRC}$( [[ -n "$GITEA_USER" ]] && echo " (user: ${GITEA_USER})")"
# Sanity: Gitea version (public, no auth)
GV="$(curl -fsS "${GITEA_URL}/api/v1/version" 2>/dev/null | jq -r '.version' 2>/dev/null || echo '?')"