#!/usr/bin/env bash # lib-forgejo.sh — Forgejo/Gitea release API helpers shared by the pangolin # release pipelines (website / server / client). `source` this after _env.sh. # # Provides: # forgejo_release_ensure -> looks up or creates the release # for <tag>; sets/exports RELEASE_ID # forgejo_upload_asset <tag> <file> -> uploads one asset to that release # # Requires env: FORGEJO_URL, FORGEJO_TOKEN, FORGEJO_REPO (see _env.sh for the # default FORGEJO_REPO=wangjia/pangolin). # # No command substitution ($()) is used anywhere: HTTP status codes and JSON # fields are written to temp files by curl/python3 and read back with `read`. # # TLS verification is ON by default. Opt-outs (env-driven, resolved once below # into the FORGEJO_CURL_TLS array and spliced into every curl call): # FORGEJO_CA_BUNDLE=<path> -> verify against this CA bundle (--cacert) # FORGEJO_INSECURE=1 -> disable verification (-k), for self-signed # internal CAs only; explicit opt-in required. : "${FORGEJO_REPO:=wangjia/pangolin}" FORGEJO_CURL_TLS=() if [ -n "${FORGEJO_CA_BUNDLE:-}" ]; then FORGEJO_CURL_TLS=(--cacert "$FORGEJO_CA_BUNDLE") else case "${FORGEJO_INSECURE:-}" in 1 | true | yes) FORGEJO_CURL_TLS=(-k) ;; *) FORGEJO_CURL_TLS=() ;; esac fi # forgejo_release_ensure <tag> <title> forgejo_release_ensure() { local tag="$1" title="$2" local get_code_file get_body_file get_code get_code_file="/tmp/forgejo_get_code.$$" get_body_file="/tmp/forgejo_get_body.$$.json" curl ${FORGEJO_CURL_TLS[@]+"${FORGEJO_CURL_TLS[@]}"} -s --max-time 60 --retry 2 --retry-connrefused -o "$get_body_file" -w '%{http_code}' \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${FORGEJO_URL}/api/v1/repos/${FORGEJO_REPO}/releases/tags/${tag}" \ > "$get_code_file" # `|| true`: curl -w '%{http_code}' 写入的值无结尾换行,read 到无换行 EOF 返回 1 # (值已赋)→ set -e 会静默中止。容错该退出码;文件恒由上面的 curl 创建,不掩盖真错。 read -r get_code < "$get_code_file" || true rm -f "$get_code_file" if [ "$get_code" = "200" ]; then echo "==> forgejo: release ${tag} already exists" _forgejo_read_release_id "$get_body_file" rm -f "$get_body_file" return 0 fi rm -f "$get_body_file" echo "==> forgejo: creating release ${tag}" local create_code_file create_body_file create_code create_req_file create_code_file="/tmp/forgejo_create_code.$$" create_body_file="/tmp/forgejo_create_body.$$.json" create_req_file="/tmp/forgejo_create_req.$$.json" # Build the JSON request body via python3's json.dumps rather than raw # string interpolation, so a tag/title containing `"` / `\` / control # characters can't break out of the JSON structure (json-injection guard). # Values are piped in NUL-separated on stdin — never interpolated into the # python source — and no $() command substitution is used. printf '%s\0%s\0' "$tag" "$title" | python3 -c ' import json import sys raw = sys.stdin.buffer.read() tag, title = (part.decode() for part in raw.split(b"\0")[:2]) json.dump( {"tag_name": tag, "name": title, "draft": False, "prerelease": False}, sys.stdout, ) ' > "$create_req_file" curl ${FORGEJO_CURL_TLS[@]+"${FORGEJO_CURL_TLS[@]}"} -s --max-time 60 -o "$create_body_file" -w '%{http_code}' \ -X POST "${FORGEJO_URL}/api/v1/repos/${FORGEJO_REPO}/releases" \ -H "Authorization: token ${FORGEJO_TOKEN}" \ -H "Content-Type: application/json" \ --data @"$create_req_file" \ > "$create_code_file" read -r create_code < "$create_code_file" || true # 无结尾换行,见 forgejo_release_ensure 注释 rm -f "$create_code_file" "$create_req_file" if [ "$create_code" -lt 200 ] || [ "$create_code" -ge 300 ]; then echo "==> forgejo: release create FAILED (HTTP ${create_code})" >&2 cat "$create_body_file" >&2 rm -f "$create_body_file" return 1 fi _forgejo_read_release_id "$create_body_file" rm -f "$create_body_file" echo "==> forgejo: release_id=${RELEASE_ID}" } # _forgejo_read_release_id <json_file> — internal: sets/exports RELEASE_ID. _forgejo_read_release_id() { local json_file="$1" id_file id_file="/tmp/forgejo_release_id.$$" python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['id'])" \ "$json_file" > "$id_file" read -r RELEASE_ID < "$id_file" || true # python 写入可能无结尾换行,见上注释 rm -f "$id_file" export RELEASE_ID } # forgejo_upload_asset <tag> <file> forgejo_upload_asset() { local tag="$1" file="$2" if [ -z "${RELEASE_ID:-}" ]; then forgejo_release_ensure "$tag" "$tag" || return 1 fi local code_file body_file code code_file="/tmp/forgejo_upload_code.$$" body_file="/tmp/forgejo_upload_body.$$.json" curl ${FORGEJO_CURL_TLS[@]+"${FORGEJO_CURL_TLS[@]}"} -s --max-time 300 -o "$body_file" -w '%{http_code}' \ -X POST "${FORGEJO_URL}/api/v1/repos/${FORGEJO_REPO}/releases/${RELEASE_ID}/assets" \ -H "Authorization: token ${FORGEJO_TOKEN}" \ -F "attachment=@${file}" \ > "$code_file" read -r code < "$code_file" || true # curl http_code 无结尾换行,见上注释 rm -f "$code_file" echo "==> forgejo: uploaded ${file} (HTTP ${code})" if [ "$code" -lt 200 ] || [ "$code" -ge 300 ]; then cat "$body_file" >&2 rm -f "$body_file" return 1 fi rm -f "$body_file" }