Files
jiu/scripts/ci/lib-forgejo.sh
T
wangjia a1a3118c88 fix(ci): download_release_assets 改用 browser_download_url + 下载后大小校验
Forgejo 的 /releases/{id}/assets/{asset_id} API 端点返回 JSON 元数据(约245字节)
而非二进制,导致下载到坏 web.tar.gz、部署时 tar 报 Unrecognized archive format。
改用 asset.browser_download_url(http→https 跟随重定向 + token)拉真正的文件,
并在下载后校验字节数与声明 size 一致,不符即当场失败。

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

160 lines
7.2 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']
# 用每个 asset 的 browser_download_url 拉真正的二进制。
# 注意:Forgejo 的 /releases/{id}/assets/{asset_id} API 端点返回的是 JSON
# 元数据(几百字节)而非文件本身——曾导致下载到 245B 的坏 web.tar.gz、
# 部署时 tar 报 "Unrecognized archive format"。browser_download_url 是
# http,会 301 到 httpsurllib 默认跟随重定向;带上 token 以防私有仓 401。
for a in rel.get('assets', []):
dl = a.get('browser_download_url')
if not dl:
raise SystemExit(f"asset {a.get('name')} 缺 browser_download_url")
req = urllib.request.Request(dl, 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())
got = os.path.getsize(f"dist/{a['name']}")
exp = a.get('size', 0)
print(f"Downloaded: {a['name']} ({got} bytes)")
if exp and got != exp:
raise SystemExit(f"下载大小不符: {a['name']} 期望 {exp} 实得 {got}")
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
}