ossutil 2.x signs requests with V4 and refuses to run without an explicit region, so the OSS mirror sync would fail in CI even with valid credentials. Derive OSS_REGION from the endpoint host (including -internal variants) and fail fast when it cannot be derived.
218 lines
9.4 KiB
Bash
Executable File
218 lines
9.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Copyright 2026 Alibaba Group
|
||
# Licensed under the Apache License, Version 2.0
|
||
#
|
||
# Sync release artifacts to a China-accessible OSS mirror. Channel pointers are
|
||
# mirror metadata; repository installers currently resolve GitHub/Gitee and do
|
||
# not consume these OSS pointers directly.
|
||
#
|
||
# Mirror layout produced:
|
||
# oss://$OSS_BUCKET/$OSS_PREFIX/download/<version>/dws-<os>-<arch>.tar.gz|.zip
|
||
# oss://$OSS_BUCKET/$OSS_PREFIX/download/<version>/checksums.txt
|
||
# oss://$OSS_BUCKET/$OSS_PREFIX/download/<version>/dws-skills.zip
|
||
# oss://$OSS_BUCKET/$OSS_PREFIX/latest.txt (stable version only)
|
||
# oss://$OSS_BUCKET/$OSS_PREFIX/beta.txt (prerelease version only)
|
||
#
|
||
# Required environment (CI secrets):
|
||
# OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET, OSS_ENDPOINT, OSS_BUCKET
|
||
# Optional:
|
||
# OSS_PREFIX (default: dws)
|
||
# DIST_DIR (default: ./dist)
|
||
# VERSION (default: derived from `git describe`)
|
||
#
|
||
# Gating: if the required OSS_* vars are unset, the script exits 0 with a notice
|
||
# (so it can be wired into release.yml without breaking forks that lack secrets).
|
||
|
||
set -eu
|
||
|
||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||
. "$SCRIPT_DIR/release-lib.sh"
|
||
|
||
DIST_DIR="${DIST_DIR:-dist}"
|
||
OSS_PREFIX="${OSS_PREFIX:-dws}"
|
||
|
||
# ── Gate: skip cleanly when credentials are not configured ────────────────────
|
||
missing=""
|
||
for v in OSS_ACCESS_KEY_ID OSS_ACCESS_KEY_SECRET OSS_ENDPOINT OSS_BUCKET; do
|
||
eval "val=\${$v:-}"
|
||
[ -z "$val" ] && missing="$missing $v"
|
||
done
|
||
if [ -n "$missing" ]; then
|
||
if [ "${DWS_REQUIRE_OSS:-0}" = "1" ]; then
|
||
echo "❌ OSS mirror sync is required but credentials are missing:${missing}" >&2
|
||
exit 1
|
||
fi
|
||
echo "ℹ️ OSS mirror sync skipped — missing:${missing}"
|
||
echo " Set these as repo secrets to auto-publish releases to the China mirror."
|
||
exit 0
|
||
fi
|
||
|
||
# ossutil v2 signs with V4 and requires an explicit region. Derive it from the
|
||
# endpoint host (oss-<region>[-internal].aliyuncs.com) unless OSS_REGION is set.
|
||
if [ -z "${OSS_REGION:-}" ]; then
|
||
OSS_REGION="$(printf '%s' "$OSS_ENDPOINT" \
|
||
| sed -n 's#^\(https\{0,1\}://\)\{0,1\}oss-\([a-z0-9-]*[a-z0-9]\)\.aliyuncs\.com.*#\2#p' \
|
||
| sed 's#-internal$##')"
|
||
fi
|
||
if [ -z "$OSS_REGION" ]; then
|
||
echo "❌ Could not derive OSS_REGION from OSS_ENDPOINT=${OSS_ENDPOINT}; set OSS_REGION explicitly." >&2
|
||
exit 1
|
||
fi
|
||
export OSS_REGION
|
||
|
||
# ── Resolve version ──────────────────────────────────────────────────────────
|
||
VERSION="${VERSION:-$(git describe --tags --always 2>/dev/null || echo dev)}"
|
||
CHANNEL="${DWS_RELEASE_CHANNEL:-$(release_channel_for_version "$VERSION")}"
|
||
release_validate_version_channel "$CHANNEL" "$VERSION"
|
||
echo "📦 Syncing release ${VERSION} → oss://${OSS_BUCKET}/${OSS_PREFIX}/download/${VERSION}/"
|
||
|
||
# Never move a channel pointer to an incomplete or mixed-version directory,
|
||
# even when this helper is invoked outside the normal workflow.
|
||
DWS_PACKAGE_DIST_DIR="$DIST_DIR" "$SCRIPT_DIR/verify-release-artifacts.sh" "$VERSION"
|
||
|
||
# ── Ensure ossutil is available ───────────────────────────────────────────────
|
||
OSSUTIL="${OSSUTIL:-ossutil}"
|
||
OSSUTIL_INSTALL_DIR=""
|
||
if ! command -v "$OSSUTIL" >/dev/null 2>&1; then
|
||
echo "⬇ ossutil not found; installing a verified temporary copy ..."
|
||
case "$(uname -s)" in
|
||
Linux) os=linux ;;
|
||
Darwin) os=mac ;;
|
||
*) printf 'unsupported ossutil operating system: %s\n' "$(uname -s)" >&2; exit 1 ;;
|
||
esac
|
||
arch="$(uname -m)"
|
||
case "$arch" in x86_64|amd64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; esac
|
||
case "${os}-${arch}" in
|
||
linux-amd64) expected_ossutil_sha256=3ae4d9fc85a7a6e9f5654d1599766f1a3a42a3692870887b5ae9338d582ef65a ;;
|
||
linux-arm64) expected_ossutil_sha256=f6c95ba0c2d2ef30290af686ce4d706c701f4734ce8090bee4288a77e3f1d764 ;;
|
||
mac-amd64) expected_ossutil_sha256=8437fdd3ef1a3eb12310f61fcf1c00a5bff5cdab47b4fea815527472e7cf896c ;;
|
||
mac-arm64) expected_ossutil_sha256=058fd048f321f8c80def8b748030531646eefe3a82837bf16b581ba7d9c84ac7 ;;
|
||
*) printf 'unsupported ossutil architecture: %s-%s\n' "$os" "$arch" >&2; exit 1 ;;
|
||
esac
|
||
ossutil_archive="$(mktemp "${TMPDIR:-/tmp}/ossutil-2.3.0.XXXXXX.zip")"
|
||
ossutil_extract="$(mktemp -d "${TMPDIR:-/tmp}/ossutil-2.3.0.XXXXXX")"
|
||
env -u OSS_ACCESS_KEY_ID -u OSS_ACCESS_KEY_SECRET \
|
||
curl -fsSL \
|
||
"https://gosspublic.alicdn.com/ossutil/v2/2.3.0/ossutil-2.3.0-${os}-${arch}.zip" \
|
||
-o "$ossutil_archive"
|
||
if command -v sha256sum >/dev/null 2>&1; then
|
||
actual_ossutil_sha256="$(sha256sum "$ossutil_archive" | awk '{print $1}')"
|
||
else
|
||
actual_ossutil_sha256="$(shasum -a 256 "$ossutil_archive" | awk '{print $1}')"
|
||
fi
|
||
[ "$actual_ossutil_sha256" = "$expected_ossutil_sha256" ] || {
|
||
rm -rf "$ossutil_archive" "$ossutil_extract"
|
||
printf 'ossutil archive checksum mismatch for %s-%s\n' "$os" "$arch" >&2
|
||
exit 1
|
||
}
|
||
unzip -qo "$ossutil_archive" -d "$ossutil_extract"
|
||
found="$(find "$ossutil_extract" -name ossutil -type f | head -1)"
|
||
[ -n "$found" ] || {
|
||
rm -rf "$ossutil_archive" "$ossutil_extract"
|
||
printf 'verified ossutil archive does not contain the executable\n' >&2
|
||
exit 1
|
||
}
|
||
OSSUTIL_INSTALL_DIR="$(mktemp -d "${TMPDIR:-/tmp}/dws-ossutil.XXXXXX")"
|
||
cp "$found" "$OSSUTIL_INSTALL_DIR/ossutil" && chmod +x "$OSSUTIL_INSTALL_DIR/ossutil"
|
||
rm -rf "$ossutil_archive" "$ossutil_extract"
|
||
OSSUTIL="$OSSUTIL_INSTALL_DIR/ossutil"
|
||
fi
|
||
|
||
oss_cp() {
|
||
# oss_cp <local-file> <oss-key>
|
||
# ossutil reads OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET from the
|
||
# environment. Never copy either secret into the process argv.
|
||
"$OSSUTIL" cp -f \
|
||
--endpoint "$OSS_ENDPOINT" \
|
||
"$1" "oss://${OSS_BUCKET}/$2"
|
||
}
|
||
|
||
oss_get() {
|
||
# oss_get <oss-key> <local-file>
|
||
"$OSSUTIL" cp -f \
|
||
--endpoint "$OSS_ENDPOINT" \
|
||
"oss://${OSS_BUCKET}/$1" "$2"
|
||
}
|
||
|
||
base="${OSS_PREFIX}/download/${VERSION}"
|
||
|
||
if [ "$CHANNEL" = "stable" ]; then
|
||
pointer_name="latest.txt"
|
||
else
|
||
pointer_name="beta.txt"
|
||
fi
|
||
current_pointer_file="$(mktemp "${TMPDIR:-/tmp}/dws-current-pointer.XXXXXX")"
|
||
pointer_file="$(mktemp "${TMPDIR:-/tmp}/dws-release-pointer.XXXXXX")"
|
||
pointer_status_file="$(mktemp "${TMPDIR:-/tmp}/dws-pointer-status.XXXXXX")"
|
||
trap 'rm -f "$current_pointer_file" "$pointer_file" "$pointer_status_file"; [ -z "$OSSUTIL_INSTALL_DIR" ] || rm -rf "$OSSUTIL_INSTALL_DIR"' EXIT HUP INT TERM
|
||
rm -f "$current_pointer_file"
|
||
current_version=""
|
||
update_pointer=1
|
||
if oss_get "${OSS_PREFIX}/${pointer_name}" "$current_pointer_file" >"$pointer_status_file" 2>&1; then
|
||
[ -s "$current_pointer_file" ] || {
|
||
cat "$pointer_status_file" >&2
|
||
echo "❌ OSS ${pointer_name} was read successfully but is empty; refusing to publish." >&2
|
||
exit 1
|
||
}
|
||
current_version="$(tr -d '[:space:]' < "$current_pointer_file")"
|
||
release_validate_version_channel "$CHANNEL" "$current_version" || {
|
||
printf '❌ OSS %s contains an invalid %s channel version: %s\n' "$pointer_name" "$CHANNEL" "$current_version" >&2
|
||
exit 1
|
||
}
|
||
if [ "$current_version" != "$VERSION" ]; then
|
||
if release_version_is_greater "$VERSION" "$current_version"; then
|
||
:
|
||
elif release_version_is_greater "$current_version" "$VERSION"; then
|
||
update_pointer=0
|
||
echo "ℹ️ OSS ${pointer_name} already points to newer ${current_version}; assets will be repaired without moving it."
|
||
else
|
||
echo "❌ Cannot order OSS ${pointer_name}=${current_version} against ${VERSION}." >&2
|
||
exit 1
|
||
fi
|
||
fi
|
||
elif grep -Eq '(^|[^[:alnum:]])NoSuchKey([^[:alnum:]]|$)' "$pointer_status_file"; then
|
||
printf 'ℹ️ OSS %s does not exist yet; creating it.\n' "$pointer_name"
|
||
else
|
||
cat "$pointer_status_file" >&2
|
||
printf '❌ Could not read OSS %s; refusing to move the channel pointer.\n' "$pointer_name" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# ── Upload binaries + checksums + skills ──────────────────────────────────────
|
||
uploaded=0
|
||
for f in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.txt; do
|
||
[ -f "$f" ] || continue
|
||
oss_cp "$f" "${base}/$(basename "$f")"
|
||
uploaded=$((uploaded + 1))
|
||
done
|
||
|
||
if [ "$uploaded" -eq 0 ]; then
|
||
echo "❌ No artifacts found in ${DIST_DIR}. Run the build (goreleaser) first." >&2
|
||
exit 1
|
||
fi
|
||
|
||
# ── Publish a channel-specific pointer ───────────────────────────────────────
|
||
# A beta must never move latest.txt: that pointer is consumed by ordinary
|
||
# installs and is the stable channel contract.
|
||
pointer_summary="${pointer_name}"
|
||
if [ "$update_pointer" -eq 1 ]; then
|
||
echo "$VERSION" > "$pointer_file"
|
||
oss_cp "$pointer_file" "${OSS_PREFIX}/${pointer_name}"
|
||
else
|
||
pointer_summary="${pointer_name} unchanged at ${current_version}"
|
||
fi
|
||
|
||
# Shared installer entrypoints are stable-only. A prerelease must not replace
|
||
# the stable entry layer, even though those scripts currently fetch from
|
||
# GitHub/Gitee rather than this asset mirror.
|
||
installer_summary=""
|
||
if [ "$CHANNEL" = "stable" ] && [ "$update_pointer" -eq 1 ]; then
|
||
for s in install.sh install.ps1 install-skills.sh; do
|
||
[ -f "scripts/$s" ] && oss_cp "scripts/$s" "${OSS_PREFIX}/$s"
|
||
done
|
||
installer_summary=" + stable install scripts"
|
||
fi
|
||
|
||
echo "✅ Synced ${uploaded} artifact(s) + ${pointer_summary}${installer_summary} to the OSS mirror."
|
||
echo " Mirror path: ${OSS_PREFIX}/download/${VERSION}/"
|