973c2c021b
Deploy Server / deploy-server (push) Successful in 3m24s
Setup Go 下载 + Forgejo GET/create/upload 的 curl 原本无 --max-time,网络停顿即 无限挂起(疑似 runner 长任务崩溃且不留日志的根因)。加 --max-time(+GET/下载 retry), 让停顿在超时内快失败并留下日志,而非静默 14 分钟后崩。POST 不 retry(避免重复建/传)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
146 lines
5.4 KiB
Bash
Executable File
146 lines
5.4 KiB
Bash
Executable File
#!/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 <tag> <title> -> 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"
|
|
}
|