Files
jiu/scripts/ci/lib-forgejo.sh
T
wangjia 2797c998f8 fix(ci): 发版 create_release/upload_asset 改幂等,重跑不再 409 崩
根因:release-client.sh 建 Release 非幂等——首跑已建好 Release+传齐产物但
deploy 未跑完时,retry 再 POST 同 tag 的 Release,Forgejo 回 409
("Release is has no Tag"),整个 release-deploy 在部署前就硬失败,发版卡死。

- create_release:撞 409 时改为按 tag 取回既有 Release 复用其 id 继续。
- upload_asset:同名 asset 已存在时先删旧再重传,避免重名 409。

重跑发版从此安全;免重编上线用 Manual Deploy(manual.yml)拉既有 Release 产物部署。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bupi8Kdqkfx2N5acFsHTx5
2026-08-28 12:21:06 +08:00

152 lines
6.6 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# lib-forgejo.sh — shared CI helpers for the three release pipelines
# (client / site / server). `source` this from release-*.sh and deploy-*.sh.
#
# Provides:
# ver_from_tag <tag> -> strips client-v|site-v|server-v|v prefix
# extract_release_notes <file> -> prints the latest "## [x]" section body
# create_release <tag> <body> -> POSTs a Forgejo Release, sets RELEASE_ID
# upload_asset <file> -> uploads one asset (needs RELEASE_ID)
# download_release_assets <tag> -> downloads all assets of a release into dist/
# setup_ssh / teardown_ssh -> prepare/clean the EC2 deploy key (sets SSH/SCP)
#
# Requires env (where relevant): FORGEJO_URL, FORGEJO_TOKEN, GITEA_REPOSITORY,
# EC2_SSH_KEY, EC2_HOST, EC2_USER.
# Strip the part-specific tag prefix, leaving a bare semver (1.2.3).
ver_from_tag() {
local tag="$1"
tag="${tag#client-v}"
tag="${tag#site-v}"
tag="${tag#server-v}"
tag="${tag#v}"
printf '%s' "$tag"
}
# Print the body (lines) of the most recent "## [ver] - date" section.
extract_release_notes() {
python3 - "$1" <<'PYEOF'
import re, sys
fn = sys.argv[1]
try:
with open(fn, encoding='utf-8') as f:
content = f.read()
parts = re.split(r'\n## ', '\n' + content)
if len(parts) > 1:
lines = parts[1].strip().split('\n')
notes = []
for line in lines[1:]:
s = line.strip()
if s.startswith('## '):
break
if s:
notes.append(s)
print('\n'.join(notes) if notes else lines[0])
else:
print('')
except Exception:
print('')
PYEOF
}
# create_release <tag> <body_json_string> — sets global RELEASE_ID on success.
# 幂等:若该 tag 的 Release 已存在(重跑发版时 Forgejo 回 409),不再硬失败,
# 改为取回既有 Release 复用其 id 继续(配合 upload_asset 的覆盖上传,重跑安全)。
create_release() {
local tag="$1" body="$2" http_code resp
echo "==> release: creating Forgejo Release ${tag}"
http_code=$(curl -k -w "%{http_code}" -o /tmp/release_resp.json \
-X POST "${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases" \
-H "Authorization: token ${FORGEJO_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${tag}\",\"name\":\"Release ${tag}\",\"body\":${body},\"draft\":false,\"prerelease\":false}")
resp=$(cat /tmp/release_resp.json)
echo "==> release: API response (HTTP ${http_code}): ${resp}"
if [ "$http_code" = "409" ]; then
# Release 已存在(重跑)→ 幂等复用:按 tag 取回既有 Release。
echo "==> release: 该 tag 的 Release 已存在,改为复用(幂等重跑)"
http_code=$(curl -k -w "%{http_code}" -o /tmp/release_resp.json \
-H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/tags/${tag}")
resp=$(cat /tmp/release_resp.json)
echo "==> release: 既有 Release 查询 (HTTP ${http_code}): ${resp}"
fi
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "==> release: FAILED — HTTP ${http_code}" >&2
return 1
fi
RELEASE_ID=$(python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" <<< "${resp}")
echo "==> release: release_id=${RELEASE_ID}"
}
# upload_asset <file> — requires RELEASE_ID.
# 幂等:若同名 asset 已存在(重跑),先删旧再传新,避免 Forgejo 409 重名冲突。
upload_asset() {
local file="$1" code name existing_id
name=$(basename "$file")
existing_id=$(curl -ks -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets" \
| python3 -c "import sys,json; a=json.load(sys.stdin); print(next((str(x['id']) for x in a if x.get('name')==sys.argv[1]), ''))" "$name" 2>/dev/null || true)
if [ -n "$existing_id" ]; then
echo "==> release: asset ${name} 已存在(id=${existing_id}),先删旧再重传"
curl -ks -X DELETE -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets/${existing_id}" >/dev/null || true
fi
code=$(curl -k -w "%{http_code}" -o /tmp/upload_resp.json \
-X POST "${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets" \
-H "Authorization: token ${FORGEJO_TOKEN}" \
-F "attachment=@${file}")
echo "==> release: uploaded ${file} (HTTP ${code}): $(cat /tmp/upload_resp.json)"
if [ "$code" -lt 200 ] || [ "$code" -ge 300 ]; then return 1; fi
}
# download_release_assets <tag> — pull every asset of the release into dist/.
download_release_assets() {
local tag="$1"
echo "==> deploy: downloading assets for ${tag} from Forgejo"
mkdir -p dist
curl -kf -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/tags/${tag}" \
-o /tmp/release-info.json
python3 - <<'PYEOF'
import json, urllib.request, ssl, os
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
rel = json.load(open('/tmp/release-info.json'))
token = os.environ['FORGEJO_TOKEN']
url = os.environ['FORGEJO_URL']
repo = os.environ['GITEA_REPOSITORY']
rid = rel['id']
for a in rel.get('assets', []):
u = f"{url}/api/v1/repos/{repo}/releases/{rid}/assets/{a['id']}"
req = urllib.request.Request(u, headers={'Authorization': f'token {token}'})
with urllib.request.urlopen(req, context=ctx) as r, open(f"dist/{a['name']}", 'wb') as o:
o.write(r.read())
print('Downloaded:', a['name'])
PYEOF
}
# setup_ssh — write the deploy key and export SSH / SCP commands.
# Target is the EC2 host by default; set DEPLOY_HOST / DEPLOY_USER / DEPLOY_SSH_KEY
# to retarget another host (e.g. the Alibaba Cloud box during the EC2→Ali migration).
# Backward-compatible: with no DEPLOY_* set, behaves exactly as before (EC2).
setup_ssh() {
local host="${DEPLOY_HOST:-$EC2_HOST}"
local key="${DEPLOY_SSH_KEY:-$EC2_SSH_KEY}"
mkdir -p ~/.ssh
# `printf '%s\n'` 末尾补一个换行:Forgejo 存 secret 会去掉结尾换行,而
# OpenSSH 格式私钥(-----BEGIN OPENSSH PRIVATE KEY-----,如阿里 ed25519 部署 key
# 缺结尾换行会被判为 "invalid format" 而拒绝加载,退化成无密钥 → Permission denied。
# 多补的换行对已含结尾换行的 PEM(如 EC2 key)无害。
printf '%s\n' "${key}" > ~/.ssh/ec2_deploy.pem
chmod 600 ~/.ssh/ec2_deploy.pem
ssh-keyscan -H "${host}" >> ~/.ssh/known_hosts 2>/dev/null
SSH="ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no"
SCP="scp -O -i ~/.ssh/ec2_deploy.pem"
}
teardown_ssh() {
rm -f ~/.ssh/ec2_deploy.pem
}