ci: unblock npm release from Gitee mirror

This commit is contained in:
修雨
2026-07-07 22:23:16 +08:00
parent 238f4256d3
commit a838ae75a7
6 changed files with 422 additions and 18 deletions
+71
View File
@@ -0,0 +1,71 @@
name: Publish npm release
on:
workflow_dispatch:
inputs:
version:
description: "Release tag to publish to npm (e.g. v1.0.48)"
required: true
type: string
permissions:
contents: read
jobs:
publish-npm:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Download GitHub release assets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eu
mkdir -p dist
gh release download "${{ inputs.version }}" \
--repo "${{ github.repository }}" \
--dir dist \
--pattern 'dws-*' \
--pattern 'checksums.txt' \
--clobber
ls -la dist
- name: Stage npm package
run: |
set -eu
version="${{ inputs.version }}"
semver="${version#v}"
pkg_root="dist/npm/dingtalk-workspace-cli"
rm -rf "$pkg_root"
mkdir -p "$pkg_root/assets" "$pkg_root/bin"
cp build/npm/install.js "$pkg_root/install.js"
cp build/npm/bin/dws.js "$pkg_root/bin/dws.js"
cp build/npm/README.md "$pkg_root/README.md"
sed "s|__VERSION__|${semver}|g" build/npm/package.json.tmpl > "$pkg_root/package.json"
cp dist/dws-* "$pkg_root/assets/"
cp dist/checksums.txt "$pkg_root/assets/"
test -f "$pkg_root/assets/dws-skills.zip"
cat "$pkg_root/package.json"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
- name: Publish stable to npm
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(inputs.version, '-') }}
working-directory: dist/npm/dingtalk-workspace-cli
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish prerelease to npm beta
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(inputs.version, '-') }}
working-directory: dist/npm/dingtalk-workspace-cli
run: npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+81 -12
View File
@@ -5,12 +5,18 @@ on:
tags:
- "v*"
workflow_dispatch:
inputs:
repair_npm_version:
description: "Only publish an existing release to npm, e.g. v1.0.48"
required: false
type: string
permissions:
contents: write
jobs:
release:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.repair_npm_version == '' }}
runs-on: ubuntu-latest
# 60 (not 30): mirroring every release asset to Gitee is slow; 30 min cut the
# Gitee step off mid-upload on the v1.0.42 release. The Gitee step is now also
@@ -76,17 +82,6 @@ jobs:
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
OSS_PREFIX: ${{ secrets.OSS_PREFIX }}
- name: Mirror release to Gitee (China)
# 把 release 附件(二进制/校验和/skills 包)镜像到 Gitee release,供 install.sh
# 的 DWS_GITEE_REPO 开关消费(仓库代码由 Gitee 仓库镜像功能自动同步,附件不在其内)。
# 脚本自带门控:未配置 GITEE_TOKEN / GITEE_REPO 时优雅跳过,不影响海外发布。
run: ./scripts/release/sync-to-gitee.sh
env:
VERSION: ${{ github.ref_name }}
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
GITEE_USER: ${{ secrets.GITEE_USER }}
GITEE_REPO: ${{ secrets.GITEE_REPO }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -94,7 +89,8 @@ jobs:
registry-url: "https://registry.npmjs.org"
- name: Publish stable to npm
# 只有官方仓库发 npm;fork(dev 预览)没有 NPM_TOKEN,跳过以免红叉
# 只有官方仓库发 npm;fork(dev 预览)没有 NPM_TOKEN,跳过以免红叉。
# 必须在 Gitee mirror 前发布:Gitee 附件上传偶发长时间挂住,不能阻塞 npm/latest。
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(github.ref_name, '-') }}
working-directory: dist/npm/dingtalk-workspace-cli
run: npm publish --access public
@@ -108,3 +104,76 @@ jobs:
run: npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Mirror release to Gitee (China)
# 把 release 附件(二进制/校验和/skills 包)镜像到 Gitee release,供 install.sh
# 的 DWS_GITEE_REPO 开关消费(仓库代码由 Gitee 仓库镜像功能自动同步,附件不在其内)。
# 默认关闭:国内 release 应由 Gitee 侧本地构建发布,避免 GitHub -> Gitee 跨境传大包卡住。
# 仅在需要临时补救时设置 repo variable ENABLE_GITEE_UPLOAD_FALLBACK=true。
if: ${{ vars.ENABLE_GITEE_UPLOAD_FALLBACK == 'true' }}
timeout-minutes: 20
run: ./scripts/release/sync-to-gitee.sh
env:
VERSION: ${{ github.ref_name }}
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
GITEE_USER: ${{ secrets.GITEE_USER }}
GITEE_REPO: ${{ secrets.GITEE_REPO }}
repair-npm:
if: ${{ github.event_name == 'workflow_dispatch' && inputs.repair_npm_version != '' }}
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Download GitHub release assets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eu
mkdir -p dist
gh release download "${{ inputs.repair_npm_version }}" \
--repo "${{ github.repository }}" \
--dir dist \
--pattern 'dws-*' \
--pattern 'checksums.txt' \
--clobber
ls -la dist
- name: Stage npm package
run: |
set -eu
version="${{ inputs.repair_npm_version }}"
semver="${version#v}"
pkg_root="dist/npm/dingtalk-workspace-cli"
rm -rf "$pkg_root"
mkdir -p "$pkg_root/assets" "$pkg_root/bin"
cp build/npm/install.js "$pkg_root/install.js"
cp build/npm/bin/dws.js "$pkg_root/bin/dws.js"
cp build/npm/README.md "$pkg_root/README.md"
sed "s|__VERSION__|${semver}|g" build/npm/package.json.tmpl > "$pkg_root/package.json"
cp dist/dws-* "$pkg_root/assets/"
cp dist/checksums.txt "$pkg_root/assets/"
test -f "$pkg_root/assets/dws-skills.zip"
cat "$pkg_root/package.json"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
- name: Publish stable to npm
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(inputs.repair_npm_version, '-') }}
working-directory: dist/npm/dingtalk-workspace-cli
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish prerelease to npm beta
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(inputs.repair_npm_version, '-') }}
working-directory: dist/npm/dingtalk-workspace-cli
run: npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+51
View File
@@ -0,0 +1,51 @@
name: Gitee Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Release tag to build on Gitee, e.g. v1.0.48"
required: false
type: string
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install packaging tools
run: |
set -eu
sudo apt-get update
sudo apt-get install -y zip unzip curl
- name: Install rcodesign
run: |
set -eu
RCS_VERSION="0.27.0"
curl -fsSL -o /tmp/rcodesign.tar.gz \
"https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F${RCS_VERSION}/apple-codesign-${RCS_VERSION}-x86_64-unknown-linux-musl.tar.gz"
mkdir -p /tmp/rcodesign
tar -xzf /tmp/rcodesign.tar.gz -C /tmp/rcodesign --strip-components=1
sudo install -m 0755 /tmp/rcodesign/rcodesign /usr/local/bin/rcodesign
rcodesign --version
- name: Build and publish Gitee release
env:
VERSION: ${{ inputs.version || github.ref_name }}
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
GITEE_REPO: DingTalk-Real-AI/dingtalk-workspace-cli
run: ./scripts/release/build-and-publish-gitee.sh
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Build release artifacts locally in the current runner and publish them to Gitee.
set -eu
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
SCRIPT_ROOT="$ROOT"
cd "$SCRIPT_ROOT"
VERSION="${VERSION:-$(git describe --tags --exact-match 2>/dev/null || true)}"
[ -n "$VERSION" ] || {
echo "error: VERSION is required or HEAD must be an exact release tag" >&2
exit 1
}
case "$VERSION" in
v*) TAG="$VERSION"; SEMVER="${VERSION#v}" ;;
*) TAG="v$VERSION"; SEMVER="$VERSION" ;;
esac
git fetch --tags origin "refs/tags/${TAG}:refs/tags/${TAG}" >/dev/null 2>&1 || true
target_commit="$(git rev-parse "${TAG}^{commit}" 2>/dev/null || true)"
[ -n "$target_commit" ] || {
echo "error: could not resolve tag ${TAG}" >&2
exit 1
}
current_commit="$(git rev-parse HEAD)"
WORKDIR="$SCRIPT_ROOT"
cleanup_worktree() {
if [ "$WORKDIR" != "$SCRIPT_ROOT" ]; then
git -C "$SCRIPT_ROOT" worktree remove --force "$WORKDIR" >/dev/null 2>&1 || rm -rf "$WORKDIR"
fi
}
trap cleanup_worktree EXIT
if [ "$current_commit" != "$target_commit" ]; then
WORKDIR="$(mktemp -d)"
rm -rf "$WORKDIR"
git worktree add --detach "$WORKDIR" "$TAG"
mkdir -p "$WORKDIR/scripts/release"
cp "$SCRIPT_ROOT/scripts/release/publish-gitee-local.sh" "$WORKDIR/scripts/release/publish-gitee-local.sh"
chmod +x "$WORKDIR/scripts/release/publish-gitee-local.sh"
fi
cd "$WORKDIR"
echo "==> Building ${TAG} locally for Gitee"
VERSION="$SEMVER" ./scripts/dev/build-all.sh
DWS_PACKAGE_VERSION="$TAG" ./scripts/release/post-goreleaser.sh
VERSION="$TAG" ./scripts/release/publish-gitee-local.sh
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
# Publish locally-built release artifacts to the matching Gitee release.
#
# Intended to run inside Gitee Go after building artifacts in China. This avoids
# the unreliable GitHub Actions -> Gitee cross-border upload path.
set -eu
DIST_DIR="${DIST_DIR:-dist}"
GITEE_API="${GITEE_API:-https://gitee.com/api/v5}"
GITEE_REPO="${GITEE_REPO:-DingTalk-Real-AI/dingtalk-workspace-cli}"
GITEE_TOKEN="${GITEE_TOKEN:-${GITEE_ACCESS_TOKEN:-}}"
GITEE_CURL_CONNECT_TIMEOUT="${GITEE_CURL_CONNECT_TIMEOUT:-15}"
GITEE_CURL_MAX_TIME="${GITEE_CURL_MAX_TIME:-120}"
GITEE_UPLOAD_MAX_TIME="${GITEE_UPLOAD_MAX_TIME:-300}"
GITEE_UPLOAD_RETRIES="${GITEE_UPLOAD_RETRIES:-3}"
GITEE_UPLOAD_RETRY_DELAY="${GITEE_UPLOAD_RETRY_DELAY:-10}"
err() {
printf 'error: %s\n' "$*" >&2
exit 1
}
[ -n "$GITEE_TOKEN" ] || err "GITEE_TOKEN is required"
[ -d "$DIST_DIR" ] || err "dist dir not found: $DIST_DIR"
VERSION="${VERSION:-$(git describe --tags --exact-match 2>/dev/null || true)}"
[ -n "$VERSION" ] || err "VERSION is required or HEAD must be an exact tag"
case "$VERSION" in
v*) ;;
*) VERSION="v$VERSION" ;;
esac
OWNER="${GITEE_REPO%%/*}"
NAME="${GITEE_REPO##*/}"
base="${GITEE_API}/repos/${OWNER}/${NAME}"
sha256_of() {
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
}
api_get() {
curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" "$@"
}
echo "📦 Publishing local artifacts for ${VERSION} to Gitee ${GITEE_REPO}"
target_commit="$(git rev-parse "${VERSION}^{commit}" 2>/dev/null || git rev-parse HEAD)"
rel_json="$(api_get "${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 --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" \
-X POST "${base}/releases" \
-F "access_token=${GITEE_TOKEN}" \
-F "tag_name=${VERSION}" \
-F "name=${VERSION}" \
-F "body=Gitee-local build of ${VERSION} for China users." \
-F "target_commitish=${target_commit}" 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" ] || err "could not get/create Gitee release for ${VERSION}. Response: ${rel_json}"
echo " Gitee release id = ${release_id}"
assets_map="$(api_get "${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)"
gitee_attach() {
file="$1"
fn="$(basename "$file")"
attempt=1
while [ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ]; do
response="$(curl -fsS --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_UPLOAD_MAX_TIME" \
--retry 2 --retry-delay 5 --retry-all-errors \
-X POST "${base}/releases/${release_id}/attach_files" \
-F "access_token=${GITEE_TOKEN}" -F "file=@${file}" 2>&1 || true)"
if printf '%s' "$response" | grep -q '"browser_download_url"'; then
return 0
fi
echo " ⚠ upload attempt ${attempt}/${GITEE_UPLOAD_RETRIES} failed for ${fn}: $(printf '%s' "$response" | head -c 240)" >&2
attempt=$((attempt + 1))
[ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ] && sleep "$GITEE_UPLOAD_RETRY_DELAY"
done
return 1
}
gitee_delete() {
curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" \
-X DELETE "${base}/releases/${release_id}/attach_files/${1}?access_token=${GITEE_TOKEN}" \
>/dev/null 2>&1 || true
}
uploaded=0
replaced=0
skipped=0
failed=0
for f in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.txt "$DIST_DIR"/dws-skills.zip; do
[ -f "$f" ] || continue
fn="$(basename "$f")"
local_sha="$(sha256_of "$f")"
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 1 ]; then
gitee_sha="$(api_get "$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 — replacing"
elif [ "$count" -gt 1 ]; then
echo " ↻ ${fn} has ${count} copies on Gitee — replacing"
else
echo " ⬆ ${fn} (new)"
fi
printf '%s\n' "$ids" | while read -r aid; do
[ -n "$aid" ] && gitee_delete "$aid"
done
if gitee_attach "$f"; then
if [ "$count" -eq 0 ]; then uploaded=$((uploaded + 1)); else replaced=$((replaced + 1)); fi
else
echo " ❌ upload failed for ${fn}" >&2
failed=$((failed + 1))
fi
done
[ "$failed" -eq 0 ] || err "Gitee publish finished with ${failed} failed upload(s)"
echo "✅ Gitee release ${VERSION}: uploaded ${uploaded}, replaced ${replaced}, skipped ${skipped}"
+25 -6
View File
@@ -28,6 +28,11 @@ set -eu
DIST_DIR="${DIST_DIR:-dist}"
GITEE_API="${GITEE_API:-https://gitee.com/api/v5}"
GITEE_CURL_CONNECT_TIMEOUT="${GITEE_CURL_CONNECT_TIMEOUT:-15}"
GITEE_CURL_MAX_TIME="${GITEE_CURL_MAX_TIME:-120}"
GITEE_UPLOAD_MAX_TIME="${GITEE_UPLOAD_MAX_TIME:-300}"
GITEE_UPLOAD_RETRIES="${GITEE_UPLOAD_RETRIES:-3}"
GITEE_UPLOAD_RETRY_DELAY="${GITEE_UPLOAD_RETRY_DELAY:-10}"
missing=""
[ -z "${GITEE_TOKEN:-}" ] && missing="$missing GITEE_TOKEN"
@@ -119,7 +124,7 @@ echo " Gitee release id = ${release_id}"
# 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 \
assets_map="$(curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" "${base}/releases/${release_id}/attach_files?access_token=${GITEE_TOKEN}" 2>/dev/null \
| python3 -c 'import json,sys
try:
data=json.load(sys.stdin)
@@ -137,13 +142,27 @@ sha256_of() { # sha256 of a file ($1) or, with no arg, of stdin
}
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"'
file="$1"
fn="$(basename "$file")"
attempt=1
while [ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ]; do
response="$(curl -fsS --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_UPLOAD_MAX_TIME" \
--retry 2 --retry-delay 5 --retry-all-errors \
-X POST "${base}/releases/${release_id}/attach_files" \
-F "access_token=${GITEE_TOKEN}" -F "file=@${file}" 2>&1 || true)"
if printf '%s' "$response" | grep -q '"browser_download_url"'; then
return 0
fi
echo " ⚠ upload attempt ${attempt}/${GITEE_UPLOAD_RETRIES} failed for ${fn}: $(printf '%s' "$response" | head -c 240)" >&2
attempt=$((attempt + 1))
[ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ] && sleep "$GITEE_UPLOAD_RETRY_DELAY"
done
return 1
}
gitee_delete() { # delete attachment by id $1
curl -fsSL -X DELETE "${base}/releases/${release_id}/attach_files/${1}?access_token=${GITEE_TOKEN}" \
curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" \
-X DELETE "${base}/releases/${release_id}/attach_files/${1}?access_token=${GITEE_TOKEN}" \
>/dev/null 2>&1 || true
}
@@ -166,7 +185,7 @@ for f in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.tx
fi
if [ "$count" -eq 1 ]; then
gitee_sha="$(curl -fsSL "$aurl" 2>/dev/null | sha256_of || true)"
gitee_sha="$(curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" "$aurl" 2>/dev/null | sha256_of || true)"
if [ "$gitee_sha" = "$local_sha" ]; then
echo " ✓ ${fn} already correct on Gitee — skip"
skipped=$((skipped + 1))