#!/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 -> strips client-v|site-v|server-v|v prefix # extract_release_notes -> prints the latest "## [x]" section body # create_release -> POSTs a Forgejo Release, sets RELEASE_ID # upload_asset -> uploads one asset (needs RELEASE_ID) # download_release_assets -> 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 — 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 — 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 — 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 到 https,urllib 默认跟随重定向;带上 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 }