Files
dingtalk-workspace-cli/scripts/release/sync-to-gitee.sh
修雨 6f042f9167 fix(release): Gitee mirror reads real attach ids + dedups duplicate assets (#502)
The verify-replace mirror listed attachments via /releases/{id}, whose
"assets" array omits the attach id. DELETE /attach_files/{id} was therefore
called with an empty id and silently no-op'd, so a stale asset was never
removed — instead a second (correct) copy was uploaded. Gitee then serves the
OLDER attachment by name, so the stale darwin binaries kept winning and failed
install.sh's checksums.txt verification on macOS (国内 install broken).

Fix:
  - List attachments via the dedicated /attach_files endpoint, which DOES
    return the numeric id needed for deletion.
  - Treat duplicates: collect every attach id carrying a given name; when >1,
    delete them all and upload exactly one fresh, correct file. count==1 still
    does the byte-identical skip / stale-replace; count==0 uploads new.

Self-heals the existing v1.0.42 darwin duplicates on the next mirror run.
2026-06-25 20:10:34 +08:00

157 lines
7.1 KiB
Bash
Executable File
Raw Permalink 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
# Copyright 2026 Alibaba Group
# Licensed under the Apache License, Version 2.0
#
# Mirror release artifacts to a Gitee release so China users can install without
# hitting GitHub. The repo code itself is kept in sync by Gitee's repo-mirror
# feature; this script handles what the mirror does NOT carry — the GitHub
# Release *attachments* (binaries, checksums, skills zip) — by uploading them to
# the matching Gitee release via the Gitee OpenAPI v5.
#
# Consumed by install.sh when DWS_GITEE_REPO is set (it resolves each asset's
# real download_url from the Gitee API, since Gitee attachment URLs carry an
# unstable numeric id).
#
# Required environment (CI secrets):
# GITEE_TOKEN Gitee private access token (scopes: projects)
# GITEE_REPO "owner/repo" on Gitee, e.g. DingTalk-Real-AI/dingtalk-workspace-cli
# Optional:
# VERSION release tag (default: git describe)
# DIST_DIR artifacts dir (default: ./dist)
# GITEE_API API base (default: https://gitee.com/api/v5)
#
# Gating: if GITEE_TOKEN / GITEE_REPO are unset, exit 0 with a notice so the
# step can live in release.yml without breaking forks that lack the secret.
set -eu
DIST_DIR="${DIST_DIR:-dist}"
GITEE_API="${GITEE_API:-https://gitee.com/api/v5}"
missing=""
[ -z "${GITEE_TOKEN:-}" ] && missing="$missing GITEE_TOKEN"
[ -z "${GITEE_REPO:-}" ] && missing="$missing GITEE_REPO"
if [ -n "$missing" ]; then
echo "ℹ️ Gitee mirror sync skipped — missing:${missing}"
echo " Set these as repo secrets to auto-mirror releases to Gitee for China users."
exit 0
fi
VERSION="${VERSION:-$(git describe --tags --always 2>/dev/null || echo dev)}"
OWNER="${GITEE_REPO%%/*}"
NAME="${GITEE_REPO##*/}"
base="${GITEE_API}/repos/${OWNER}/${NAME}"
echo "📦 Mirroring release ${VERSION} → Gitee ${GITEE_REPO}"
# ── Resolve or create the Gitee release for this tag ──────────────────────────
# Gitee mirror sync brings the git tag over, so the tag should already exist.
rel_json="$(curl -fsSL "${base}/releases/tags/${VERSION}?access_token=${GITEE_TOKEN}" 2>/dev/null || true)"
release_id="$(printf '%s' "$rel_json" | grep -o '"id":[ ]*[0-9]*' | head -1 | grep -o '[0-9]*' || true)"
if [ -z "$release_id" ]; then
echo " No Gitee release for ${VERSION} yet — creating it."
rel_json="$(curl -fsSL -X POST "${base}/releases" \
-F "access_token=${GITEE_TOKEN}" \
-F "tag_name=${VERSION}" \
-F "name=${VERSION}" \
-F "body=Mirror of GitHub release ${VERSION} for China users." \
-F "target_commitish=main" 2>/dev/null || true)"
release_id="$(printf '%s' "$rel_json" | grep -o '"id":[ ]*[0-9]*' | head -1 | grep -o '[0-9]*' || true)"
fi
[ -n "$release_id" ] || { echo "❌ Could not get/create Gitee release for ${VERSION}. Response: ${rel_json}" >&2; exit 1; }
echo " Gitee release id = ${release_id}"
# ── Mirror each artifact, verifying content so re-runs self-heal ─────────────
# Pull the current attachment list (name + attach id + download url) so we can,
# per file:
# • skip it when it is already on Gitee, unique, AND byte-identical,
# • REPLACE it when present but stale (different bytes) — e.g. the darwin
# binaries are re-signed and so differ between GitHub and a prior mirror
# run, which breaks install.sh's checksums.txt verification on macOS,
# • DEDUP it when the same name has >1 attachment — a prior run that failed to
# delete (see below) left the old copy *and* an extra upload; Gitee then
# serves the OLDER one by name, so the stale binary wins. We delete every
# copy and upload one fresh.
# • upload it when missing.
# This brings the Gitee release into byte-for-byte agreement with $DIST_DIR,
# which the caller fills from the GitHub release whose checksums.txt is what
# install.sh verifies against.
#
# We list attachments via the dedicated /attach_files endpoint, NOT the release
# detail (/releases/{id}) endpoint: the latter's "assets" array omits the attach
# id, so DELETE /attach_files/{id} was previously called with an empty id and
# silently no-op'd — leaving stale + duplicate darwin binaries on Gitee.
assets_map="$(curl -fsSL "${base}/releases/${release_id}/attach_files?access_token=${GITEE_TOKEN}" 2>/dev/null \
| python3 -c 'import json,sys
try:
data=json.load(sys.stdin)
rows=data if isinstance(data,list) else data.get("attach_files",[])
for a in rows:
n=a.get("name",""); i=a.get("id",""); u=a.get("browser_download_url","")
if n and i!="":
print("%s\t%s\t%s" % (n, i, u))
except Exception:
pass' 2>/dev/null || true)"
sha256_of() { # sha256 of a file ($1) or, with no arg, of stdin
if command -v sha256sum >/dev/null 2>&1; then sha256sum ${1:+"$1"} | awk '{print $1}';
else shasum -a 256 ${1:+"$1"} | awk '{print $1}'; fi
}
gitee_attach() { # upload file $1; success when the response carries a download url
printf '%s' "$(curl -fsSL -X POST "${base}/releases/${release_id}/attach_files" \
-F "access_token=${GITEE_TOKEN}" -F "file=@${1}" 2>/dev/null || true)" \
| grep -q '"browser_download_url"'
}
gitee_delete() { # delete attachment by id $1
curl -fsSL -X DELETE "${base}/releases/${release_id}/attach_files/${1}?access_token=${GITEE_TOKEN}" \
>/dev/null 2>&1 || true
}
uploaded=0
replaced=0
skipped=0
for f in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.txt; do
[ -f "$f" ] || continue
fn="$(basename "$f")"
local_sha="$(sha256_of "$f")"
# Every attach id currently carrying this name (may be >1 from a botched run).
ids="$(printf '%s\n' "$assets_map" | awk -F'\t' -v n="$fn" '$1==n {print $2}')"
aurl="$(printf '%s\n' "$assets_map" | awk -F'\t' -v n="$fn" '$1==n {print $3; exit}')"
count="$(printf '%s' "$ids" | grep -c . || true)"
if [ "$count" -eq 0 ]; then
echo " ⬆ ${fn} (new)"
if gitee_attach "$f"; then uploaded=$((uploaded + 1)); else echo " ⚠ upload may have failed for ${fn}" >&2; fi
continue
fi
if [ "$count" -eq 1 ]; then
gitee_sha="$(curl -fsSL "$aurl" 2>/dev/null | sha256_of || true)"
if [ "$gitee_sha" = "$local_sha" ]; then
echo " ✓ ${fn} already correct on Gitee — skip"
skipped=$((skipped + 1))
continue
fi
echo " ↻ ${fn} differs on Gitee (stale) — deleting + re-uploading"
else
echo " ↻ ${fn} has ${count} copies on Gitee (dup) — deleting all + re-uploading one"
fi
# Delete every copy, then upload exactly one fresh, correct file.
printf '%s\n' "$ids" | while read -r aid; do
[ -n "$aid" ] && gitee_delete "$aid"
done
if gitee_attach "$f"; then replaced=$((replaced + 1)); else echo " ⚠ re-upload may have failed for ${fn}" >&2; fi
done
if [ "$uploaded" -eq 0 ] && [ "$replaced" -eq 0 ] && [ "$skipped" -eq 0 ]; then
echo "❌ No artifacts found to mirror. Did the build (goreleaser) run / were assets downloaded into ${DIST_DIR}?" >&2
exit 1
fi
echo "✅ Gitee release ${VERSION}: uploaded ${uploaded}, replaced ${replaced}, skipped ${skipped} (already correct)."
echo " China install: DWS_GITEE_REPO=${GITEE_REPO} \\"
echo " curl -fsSL https://gitee.com/${GITEE_REPO}/raw/main/scripts/install.sh | sh"