Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eda235f62 |
@@ -0,0 +1,54 @@
|
|||||||
|
name: Verify Gitee release
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 2 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Release tag to verify; defaults to the latest stable GitHub release"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Resolve release version
|
||||||
|
id: release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
INPUT_VERSION: ${{ inputs.version }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
version="$INPUT_VERSION"
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
version="$(gh release view --repo "${{ github.repository }}" --json tagName --jq .tagName)"
|
||||||
|
fi
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Download GitHub release assets
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
mkdir -p dist
|
||||||
|
gh release download "${{ steps.release.outputs.version }}" \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--dir dist \
|
||||||
|
--pattern 'dws-*' \
|
||||||
|
--pattern 'checksums.txt' \
|
||||||
|
--clobber
|
||||||
|
|
||||||
|
- name: Verify matching Gitee release
|
||||||
|
run: ./scripts/release/verify-gitee-release.sh
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.release.outputs.version }}
|
||||||
|
GITEE_REPO: DingTalk-Real-AI/dingtalk-workspace-cli
|
||||||
@@ -50,6 +50,40 @@ gitee_api() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gitee_release_has_assets() {
|
||||||
|
_version="$1"
|
||||||
|
shift
|
||||||
|
_release="$(gitee_api "https://gitee.com/api/v5/repos/${GITEE_REPO}/releases/tags/${_version}" 2>/dev/null || true)"
|
||||||
|
[ -n "$_release" ] && [ "$_release" != "null" ] || return 1
|
||||||
|
_compact="$(printf '%s' "$_release" | tr -d '[:space:]')"
|
||||||
|
for _asset in "$@"; do
|
||||||
|
printf '%s' "$_compact" | grep -Fq "\"name\":\"${_asset}\"" || return 1
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_gitee_latest_version() {
|
||||||
|
_tags="$(gitee_api "https://gitee.com/api/v5/repos/${GITEE_REPO}/tags" 2>/dev/null || true)"
|
||||||
|
_candidates="$(printf '%s' "$_tags" \
|
||||||
|
| grep -o '"name":[ ]*"v[0-9][0-9.]*"' \
|
||||||
|
| sed 's/.*"name":[ ]*"//;s/"$//' \
|
||||||
|
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||||
|
| sort -Vr)"
|
||||||
|
_newest="$(printf '%s\n' "$_candidates" | head -1)"
|
||||||
|
[ -n "$_newest" ] || return 1
|
||||||
|
|
||||||
|
for _candidate in $_candidates; do
|
||||||
|
if gitee_release_has_assets "$_candidate" "$@"; then
|
||||||
|
VERSION="$_candidate"
|
||||||
|
if [ "$_candidate" != "$_newest" ]; then
|
||||||
|
printf ' ⚠ Gitee tag %s has no complete release assets; using %s.\n' "$_newest" "$_candidate"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
pick_source() {
|
pick_source() {
|
||||||
[ -n "$GITEE_REPO" ] && return 0
|
[ -n "$GITEE_REPO" ] && return 0
|
||||||
[ "${DWS_NO_FALLBACK:-0}" = "1" ] && return 0
|
[ "${DWS_NO_FALLBACK:-0}" = "1" ] && return 0
|
||||||
@@ -61,13 +95,7 @@ pick_source() {
|
|||||||
resolve_version() {
|
resolve_version() {
|
||||||
if [ "$VERSION" = "latest" ]; then
|
if [ "$VERSION" = "latest" ]; then
|
||||||
if [ -n "$GITEE_REPO" ]; then
|
if [ -n "$GITEE_REPO" ]; then
|
||||||
# Gitee's /releases/latest and /releases endpoints are unreliable, so
|
resolve_gitee_latest_version "$@" || VERSION=""
|
||||||
# resolve the newest vN.N.N tag from the git tags endpoint instead.
|
|
||||||
VERSION="$(gitee_api "https://gitee.com/api/v5/repos/${GITEE_REPO}/tags" \
|
|
||||||
| grep -o '"name":[ ]*"v[0-9][0-9.]*"' \
|
|
||||||
| sed 's/.*"name":[ ]*"//;s/"$//' \
|
|
||||||
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
|
||||||
| sort -V | tail -1)"
|
|
||||||
else
|
else
|
||||||
VERSION="$(curl -fsSI "https://github.com/${REPO}/releases/latest" 2>/dev/null \
|
VERSION="$(curl -fsSI "https://github.com/${REPO}/releases/latest" 2>/dev/null \
|
||||||
| grep -i '^location:' | sed 's|.*/tag/||;s/[[:space:]]*$//')"
|
| grep -i '^location:' | sed 's|.*/tag/||;s/[[:space:]]*$//')"
|
||||||
@@ -210,7 +238,7 @@ install_skills_to_root() {
|
|||||||
main() {
|
main() {
|
||||||
need_cmd curl
|
need_cmd curl
|
||||||
pick_source
|
pick_source
|
||||||
resolve_version
|
resolve_version dws-skills.zip
|
||||||
|
|
||||||
printf '\n'
|
printf '\n'
|
||||||
printf ' ┌──────────────────────────────────────┐\n'
|
printf ' ┌──────────────────────────────────────┐\n'
|
||||||
|
|||||||
+39
-10
@@ -152,6 +152,24 @@ function Get-GiteeAssetUrl {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Test-GiteeReleaseAssets {
|
||||||
|
param(
|
||||||
|
[string]$Candidate,
|
||||||
|
[string[]]$RequiredAssets
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
$release = Invoke-GiteeApi "https://gitee.com/api/v5/repos/$GiteeRepo/releases/tags/$Candidate"
|
||||||
|
} catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
if (-not $release -or -not $release.assets) { return $false }
|
||||||
|
$assetNames = @($release.assets | ForEach-Object { $_.name })
|
||||||
|
foreach ($required in $RequiredAssets) {
|
||||||
|
if ($assetNames -notcontains $required) { return $false }
|
||||||
|
}
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
function Resolve-Source {
|
function Resolve-Source {
|
||||||
# Explicit DWS_GITEE_REPO wins; else probe GitHub and fall back to Gitee when unreachable.
|
# Explicit DWS_GITEE_REPO wins; else probe GitHub and fall back to Gitee when unreachable.
|
||||||
if ($GiteeRepo -ne "") { return }
|
if ($GiteeRepo -ne "") { return }
|
||||||
@@ -167,18 +185,26 @@ function Resolve-Source {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Resolve-LatestVersion {
|
function Resolve-LatestVersion {
|
||||||
|
param([string[]]$RequiredAssets = @())
|
||||||
if ($Version -eq "latest") {
|
if ($Version -eq "latest") {
|
||||||
if ($GiteeRepo -ne "") {
|
if ($GiteeRepo -ne "") {
|
||||||
try {
|
try {
|
||||||
# Gitee's /releases/latest and /releases endpoints are unreliable
|
# Tags can be mirrored before release attachments. Select the
|
||||||
# (404 / empty even when releases exist), so resolve the newest
|
# newest stable tag with all assets required by this install.
|
||||||
# vN.N.N tag from the git tags endpoint instead.
|
|
||||||
$tags = Invoke-GiteeApi "https://gitee.com/api/v5/repos/$GiteeRepo/tags"
|
$tags = Invoke-GiteeApi "https://gitee.com/api/v5/repos/$GiteeRepo/tags"
|
||||||
$latest = $tags.name |
|
$candidates = @($tags.name |
|
||||||
Where-Object { $_ -match '^v\d+\.\d+\.\d+$' } |
|
Where-Object { $_ -match '^v\d+\.\d+\.\d+$' } |
|
||||||
ForEach-Object { [version]($_.TrimStart('v')) } |
|
Sort-Object { [version]($_.TrimStart('v')) } -Descending)
|
||||||
Sort-Object | Select-Object -Last 1
|
$newest = $candidates | Select-Object -First 1
|
||||||
if ($latest) { $script:Version = "v$latest"; return }
|
foreach ($candidate in $candidates) {
|
||||||
|
if (Test-GiteeReleaseAssets $candidate $RequiredAssets) {
|
||||||
|
$script:Version = $candidate
|
||||||
|
if ($candidate -ne $newest) {
|
||||||
|
Write-Say "⚠ Gitee tag $newest has no complete release assets; using $candidate."
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
Write-Err "Could not determine the latest Gitee version. Set `$env:DWS_VERSION explicitly."
|
Write-Err "Could not determine the latest Gitee version. Set `$env:DWS_VERSION explicitly."
|
||||||
return
|
return
|
||||||
@@ -349,9 +375,12 @@ function Write-MultiModeNotice {
|
|||||||
|
|
||||||
function Install-Binary {
|
function Install-Binary {
|
||||||
$arch = Get-Arch
|
$arch = Get-Arch
|
||||||
Resolve-LatestVersion
|
|
||||||
|
|
||||||
$archiveName = "${BinName}-windows-${arch}.zip"
|
$archiveName = "${BinName}-windows-${arch}.zip"
|
||||||
|
$requiredAssets = @($archiveName, "checksums.txt")
|
||||||
|
if (!$NoSkills -and $SkillMode -ne "multi") {
|
||||||
|
$requiredAssets += "dws-skills.zip"
|
||||||
|
}
|
||||||
|
Resolve-LatestVersion $requiredAssets
|
||||||
if ($GiteeRepo -ne "") { $downloadUrl = Get-GiteeAssetUrl $archiveName } else { $downloadUrl = "https://github.com/$Repo/releases/download/$Version/$archiveName" }
|
if ($GiteeRepo -ne "") { $downloadUrl = Get-GiteeAssetUrl $archiveName } else { $downloadUrl = "https://github.com/$Repo/releases/download/$Version/$archiveName" }
|
||||||
if (-not $downloadUrl) { Write-Err "Could not resolve download URL for $archiveName (version $Version)." }
|
if (-not $downloadUrl) { Write-Err "Could not resolve download URL for $archiveName (version $Version)." }
|
||||||
|
|
||||||
@@ -545,7 +574,7 @@ function Install-BinaryFromSource {
|
|||||||
function Install-Skills {
|
function Install-Skills {
|
||||||
Write-Say ""
|
Write-Say ""
|
||||||
Write-Say "📦 Installing agent skills from GitHub Releases..."
|
Write-Say "📦 Installing agent skills from GitHub Releases..."
|
||||||
Resolve-LatestVersion
|
Resolve-LatestVersion @("dws-skills.zip")
|
||||||
|
|
||||||
if ($GiteeRepo -ne "") { $zipUrl = Get-GiteeAssetUrl "dws-skills.zip" } else { $zipUrl = "https://github.com/$Repo/releases/download/$Version/dws-skills.zip" }
|
if ($GiteeRepo -ne "") { $zipUrl = Get-GiteeAssetUrl "dws-skills.zip" } else { $zipUrl = "https://github.com/$Repo/releases/download/$Version/dws-skills.zip" }
|
||||||
if (-not $zipUrl) { Write-Err "Could not resolve download URL for dws-skills.zip (version $Version)." }
|
if (-not $zipUrl) { Write-Err "Could not resolve download URL for dws-skills.zip (version $Version)." }
|
||||||
|
|||||||
+44
-13
@@ -105,6 +105,40 @@ gitee_api() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gitee_release_has_assets() {
|
||||||
|
_version="$1"
|
||||||
|
shift
|
||||||
|
_release="$(gitee_api "https://gitee.com/api/v5/repos/${GITEE_REPO}/releases/tags/${_version}" 2>/dev/null || true)"
|
||||||
|
[ -n "$_release" ] && [ "$_release" != "null" ] || return 1
|
||||||
|
_compact="$(printf '%s' "$_release" | tr -d '[:space:]')"
|
||||||
|
for _asset in "$@"; do
|
||||||
|
printf '%s' "$_compact" | grep -Fq "\"name\":\"${_asset}\"" || return 1
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_gitee_latest_version() {
|
||||||
|
_tags="$(gitee_api "https://gitee.com/api/v5/repos/${GITEE_REPO}/tags" 2>/dev/null || true)"
|
||||||
|
_candidates="$(printf '%s' "$_tags" \
|
||||||
|
| grep -o '"name":[ ]*"v[0-9][0-9.]*"' \
|
||||||
|
| sed 's/.*"name":[ ]*"//;s/"$//' \
|
||||||
|
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||||
|
| sort -Vr)"
|
||||||
|
_newest="$(printf '%s\n' "$_candidates" | head -1)"
|
||||||
|
[ -n "$_newest" ] || return 1
|
||||||
|
|
||||||
|
for _candidate in $_candidates; do
|
||||||
|
if gitee_release_has_assets "$_candidate" "$@"; then
|
||||||
|
VERSION="$_candidate"
|
||||||
|
if [ "$_candidate" != "$_newest" ]; then
|
||||||
|
say "⚠ Gitee tag ${_newest} has no complete release assets; using ${_candidate}."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
# Resolve the download URL of a release asset by file name.
|
# Resolve the download URL of a release asset by file name.
|
||||||
# GitHub: deterministic template <repo>/releases/download/<version>/<file>.
|
# GitHub: deterministic template <repo>/releases/download/<version>/<file>.
|
||||||
# Gitee : query the release API and pick the asset whose name matches
|
# Gitee : query the release API and pick the asset whose name matches
|
||||||
@@ -173,19 +207,13 @@ pick_source() {
|
|||||||
say "⚠ GitHub 不可达,自动切换国内 Gitee 镜像: ${GITEE_REPO}"
|
say "⚠ GitHub 不可达,自动切换国内 Gitee 镜像: ${GITEE_REPO}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve the latest version tag from GitHub
|
# Resolve the latest installable release from the selected source.
|
||||||
resolve_version() {
|
resolve_version() {
|
||||||
if [ "$VERSION" = "latest" ]; then
|
if [ "$VERSION" = "latest" ]; then
|
||||||
if [ -n "$GITEE_REPO" ]; then
|
if [ -n "$GITEE_REPO" ]; then
|
||||||
# Gitee's /releases/latest and /releases endpoints are unreliable — they
|
# Tags can be mirrored before release attachments. Select the newest
|
||||||
# return 404 / an empty list even when releases exist — so resolve the
|
# stable tag whose release contains every asset needed by this install.
|
||||||
# newest version from the git tags endpoint instead: keep only vN.N.N
|
resolve_gitee_latest_version "$@" || VERSION=""
|
||||||
# tags and pick the highest by version order.
|
|
||||||
VERSION="$(gitee_api "https://gitee.com/api/v5/repos/${GITEE_REPO}/tags" \
|
|
||||||
| grep -o '"name":[ ]*"v[0-9][0-9.]*"' \
|
|
||||||
| sed 's/.*"name":[ ]*"//;s/"$//' \
|
|
||||||
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
|
||||||
| sort -V | tail -1)"
|
|
||||||
elif need_cmd curl; then
|
elif need_cmd curl; then
|
||||||
# Follow the redirect from /releases/latest to get the tag
|
# Follow the redirect from /releases/latest to get the tag
|
||||||
VERSION="$(curl -fsSI "https://github.com/${REPO}/releases/latest" 2>/dev/null \
|
VERSION="$(curl -fsSI "https://github.com/${REPO}/releases/latest" 2>/dev/null \
|
||||||
@@ -472,9 +500,12 @@ _copy_skill() {
|
|||||||
install_binary() {
|
install_binary() {
|
||||||
os="$(detect_os)"
|
os="$(detect_os)"
|
||||||
arch="$(detect_arch)"
|
arch="$(detect_arch)"
|
||||||
resolve_version
|
|
||||||
|
|
||||||
archive_name="${BIN_NAME}-${os}-${arch}.tar.gz"
|
archive_name="${BIN_NAME}-${os}-${arch}.tar.gz"
|
||||||
|
if [ "$NO_SKILLS" != "1" ] && [ "$SKILL_MODE" != "multi" ]; then
|
||||||
|
resolve_version "$archive_name" checksums.txt dws-skills.zip
|
||||||
|
else
|
||||||
|
resolve_version "$archive_name" checksums.txt
|
||||||
|
fi
|
||||||
download_url="$(asset_url "$archive_name")"
|
download_url="$(asset_url "$archive_name")"
|
||||||
[ -n "$download_url" ] || err "Could not resolve download URL for ${archive_name} (version ${VERSION})."
|
[ -n "$download_url" ] || err "Could not resolve download URL for ${archive_name} (version ${VERSION})."
|
||||||
|
|
||||||
@@ -555,8 +586,8 @@ install_skills() {
|
|||||||
say ""
|
say ""
|
||||||
say "📦 Installing agent skills from GitHub Releases..."
|
say "📦 Installing agent skills from GitHub Releases..."
|
||||||
|
|
||||||
resolve_version
|
|
||||||
skills_archive="dws-skills.zip"
|
skills_archive="dws-skills.zip"
|
||||||
|
resolve_version "$skills_archive"
|
||||||
download_url="$(asset_url "$skills_archive")"
|
download_url="$(asset_url "$skills_archive")"
|
||||||
[ -n "$download_url" ] || err "Could not resolve download URL for ${skills_archive} (version ${VERSION})."
|
[ -n "$download_url" ] || err "Could not resolve download URL for ${skills_archive} (version ${VERSION})."
|
||||||
|
|
||||||
|
|||||||
@@ -142,3 +142,6 @@ done
|
|||||||
|
|
||||||
[ "$failed" -eq 0 ] || err "Gitee publish finished with ${failed} failed upload(s)"
|
[ "$failed" -eq 0 ] || err "Gitee publish finished with ${failed} failed upload(s)"
|
||||||
echo "✅ Gitee release ${VERSION}: uploaded ${uploaded}, replaced ${replaced}, skipped ${skipped}"
|
echo "✅ Gitee release ${VERSION}: uploaded ${uploaded}, replaced ${replaced}, skipped ${skipped}"
|
||||||
|
VERSION="$VERSION" GITEE_REPO="$GITEE_REPO" GITEE_API="$GITEE_API" \
|
||||||
|
GITEE_TOKEN="$GITEE_TOKEN" DIST_DIR="$DIST_DIR" \
|
||||||
|
./scripts/release/verify-gitee-release.sh
|
||||||
|
|||||||
@@ -208,5 +208,8 @@ if [ "$uploaded" -eq 0 ] && [ "$replaced" -eq 0 ] && [ "$skipped" -eq 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "✅ Gitee release ${VERSION}: uploaded ${uploaded}, replaced ${replaced}, skipped ${skipped} (already correct)."
|
echo "✅ Gitee release ${VERSION}: uploaded ${uploaded}, replaced ${replaced}, skipped ${skipped} (already correct)."
|
||||||
|
VERSION="$VERSION" GITEE_REPO="$GITEE_REPO" GITEE_API="$GITEE_API" \
|
||||||
|
GITEE_TOKEN="$GITEE_TOKEN" DIST_DIR="$DIST_DIR" \
|
||||||
|
./scripts/release/verify-gitee-release.sh
|
||||||
echo " China install: DWS_GITEE_REPO=${GITEE_REPO} \\"
|
echo " China install: DWS_GITEE_REPO=${GITEE_REPO} \\"
|
||||||
echo " curl -fsSL https://gitee.com/${GITEE_REPO}/raw/main/scripts/install.sh | sh"
|
echo " curl -fsSL https://gitee.com/${GITEE_REPO}/raw/main/scripts/install.sh | sh"
|
||||||
|
|||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Verify that a Gitee release exposes every standard DWS release asset.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
VERSION="${VERSION:-}"
|
||||||
|
GITEE_REPO="${GITEE_REPO:-DingTalk-Real-AI/dingtalk-workspace-cli}"
|
||||||
|
GITEE_API="${GITEE_API:-https://gitee.com/api/v5}"
|
||||||
|
DIST_DIR="${DIST_DIR:-dist}"
|
||||||
|
GITEE_VERIFY_RETRIES="${GITEE_VERIFY_RETRIES:-6}"
|
||||||
|
GITEE_VERIFY_RETRY_DELAY="${GITEE_VERIFY_RETRY_DELAY:-5}"
|
||||||
|
REQUIRED_ASSETS="${GITEE_REQUIRED_ASSETS:-checksums.txt dws-darwin-amd64.tar.gz dws-darwin-arm64.tar.gz dws-linux-amd64.tar.gz dws-linux-arm64.tar.gz dws-skills.zip dws-windows-amd64.zip dws-windows-arm64.zip}"
|
||||||
|
|
||||||
|
[ -n "$VERSION" ] || {
|
||||||
|
echo "error: VERSION is required" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
for asset in $REQUIRED_ASSETS; do
|
||||||
|
[ -f "$DIST_DIR/$asset" ] || {
|
||||||
|
echo "error: local release asset is missing: $DIST_DIR/$asset" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
endpoint="${GITEE_API}/repos/${GITEE_REPO}/releases/tags/${VERSION}"
|
||||||
|
if [ -n "${GITEE_TOKEN:-}" ]; then
|
||||||
|
endpoint="${endpoint}?access_token=${GITEE_TOKEN}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
attempt=1
|
||||||
|
missing=""
|
||||||
|
while [ "$attempt" -le "$GITEE_VERIFY_RETRIES" ]; do
|
||||||
|
release_json="$(curl -fsSL "$endpoint" 2>/dev/null || true)"
|
||||||
|
compact="$(printf '%s' "$release_json" | tr -d '[:space:]')"
|
||||||
|
missing=""
|
||||||
|
for asset in $REQUIRED_ASSETS; do
|
||||||
|
count="$(printf '%s' "$compact" | grep -oF "\"name\":\"${asset}\"" | wc -l | tr -d ' ')"
|
||||||
|
if [ "$count" -ne 1 ]; then
|
||||||
|
missing="${missing} ${asset}(count=${count})"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -n "$release_json" ] && [ "$release_json" != "null" ] && [ -z "$missing" ]; then
|
||||||
|
echo "Gitee release ${VERSION} is complete (${GITEE_REPO})."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
[ "$attempt" -le "$GITEE_VERIFY_RETRIES" ] && sleep "$GITEE_VERIFY_RETRY_DELAY"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "error: Gitee release ${VERSION} is incomplete:${missing:- release not found}" >&2
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package scripts_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerifyGiteeReleaseRequiresEveryAsset(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
remote string
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "complete",
|
||||||
|
remote: `{"assets":[{"name":"checksums.txt"},{"name":"dws-linux-amd64.tar.gz"},{"name":"dws-skills.zip"}]}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing remote asset",
|
||||||
|
remote: `{"assets":[{"name":"checksums.txt"},{"name":"dws-linux-amd64.tar.gz"}]}`,
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
distDir := filepath.Join(root, "dist")
|
||||||
|
for _, name := range []string{"checksums.txt", "dws-linux-amd64.tar.gz", "dws-skills.zip"} {
|
||||||
|
mustWriteFile(t, filepath.Join(distDir, name), []byte(name), 0o644)
|
||||||
|
}
|
||||||
|
stubRoot := filepath.Join(root, "stubs")
|
||||||
|
mustWriteFile(t, filepath.Join(stubRoot, "curl"), []byte("#!/bin/sh\nprintf '%s' \"$FAKE_GITEE_RELEASE\"\n"), 0o755)
|
||||||
|
|
||||||
|
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "verify-gitee-release.sh"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Abs(verify-gitee-release.sh) error = %v", err)
|
||||||
|
}
|
||||||
|
cmd := exec.Command("sh", scriptPath)
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
"PATH="+stubRoot+":"+os.Getenv("PATH"),
|
||||||
|
"VERSION=v1.2.3",
|
||||||
|
"DIST_DIR="+distDir,
|
||||||
|
"GITEE_REQUIRED_ASSETS=checksums.txt dws-linux-amd64.tar.gz dws-skills.zip",
|
||||||
|
"GITEE_VERIFY_RETRIES=1",
|
||||||
|
"FAKE_GITEE_RELEASE="+tt.remote,
|
||||||
|
)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if tt.wantError {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("verify-gitee-release.sh error = nil, want failure\noutput:\n%s", string(output))
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output), "dws-skills.zip(count=0)") {
|
||||||
|
t.Fatalf("missing asset not reported:\n%s", string(output))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("verify-gitee-release.sh error = %v\noutput:\n%s", err, string(output))
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output), "Gitee release v1.2.3 is complete") {
|
||||||
|
t.Fatalf("success output missing completeness result:\n%s", string(output))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGiteePublishersRunCompletenessVerification(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
for _, relPath := range []string{
|
||||||
|
filepath.Join("..", "..", "scripts", "release", "sync-to-gitee.sh"),
|
||||||
|
filepath.Join("..", "..", "scripts", "release", "publish-gitee-local.sh"),
|
||||||
|
filepath.Join("..", "..", ".github", "workflows", "verify-gitee-release.yml"),
|
||||||
|
} {
|
||||||
|
data, err := os.ReadFile(relPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(%s) error = %v", relPath, err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), "verify-gitee-release.sh") {
|
||||||
|
t.Fatalf("%s does not run verify-gitee-release.sh", relPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"archive/tar"
|
"archive/tar"
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -191,6 +193,104 @@ func TestInstallScriptsUseGitHubReleaseSkillsAsset(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInstallScriptGiteeLatestUsesNewestCompleteRelease(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("POSIX shell installer test is for unix-like hosts")
|
||||||
|
}
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
installDir := filepath.Join(root, "bin")
|
||||||
|
stubRoot := filepath.Join(root, "stubs")
|
||||||
|
releaseDir := filepath.Join(root, "release")
|
||||||
|
archiveName := "dws-linux-amd64.tar.gz"
|
||||||
|
archivePath := filepath.Join(releaseDir, archiveName)
|
||||||
|
writeTarGz(t, archivePath, map[string]string{
|
||||||
|
"dws": "#!/bin/sh\nprintf 'Version: v1.0.49\\n'\n",
|
||||||
|
})
|
||||||
|
archiveData, err := os.ReadFile(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(%s) error = %v", archivePath, err)
|
||||||
|
}
|
||||||
|
checksum := fmt.Sprintf("%x %s\n", sha256.Sum256(archiveData), archiveName)
|
||||||
|
mustWriteFile(t, filepath.Join(releaseDir, "checksums.txt"), []byte(checksum), 0o644)
|
||||||
|
|
||||||
|
scriptData, err := os.ReadFile(filepath.Join("..", "..", "scripts", "install.sh"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(install.sh) error = %v", err)
|
||||||
|
}
|
||||||
|
scriptPath := filepath.Join(root, "standalone", "install.sh")
|
||||||
|
mustWriteFile(t, scriptPath, scriptData, 0o755)
|
||||||
|
writeFakeGiteeCurl(t, filepath.Join(stubRoot, "curl"))
|
||||||
|
mustWriteFile(t, filepath.Join(stubRoot, "uname"), []byte(`#!/bin/sh
|
||||||
|
case "${1:-}" in
|
||||||
|
-s) echo Linux ;;
|
||||||
|
-m) echo x86_64 ;;
|
||||||
|
*) echo Linux ;;
|
||||||
|
esac
|
||||||
|
`), 0o755)
|
||||||
|
|
||||||
|
cmd := exec.Command("sh", scriptPath)
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
"HOME="+filepath.Join(root, "home"),
|
||||||
|
"PATH="+stubRoot+":"+os.Getenv("PATH"),
|
||||||
|
"DWS_INSTALL_DIR="+installDir,
|
||||||
|
"DWS_GITEE_REPO=example/dws",
|
||||||
|
"DWS_VERSION=latest",
|
||||||
|
"DWS_NO_SKILLS=1",
|
||||||
|
"FAKE_RELEASE_DIR="+releaseDir,
|
||||||
|
)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("install.sh error = %v\noutput:\n%s", err, string(output))
|
||||||
|
}
|
||||||
|
got := string(output)
|
||||||
|
if !strings.Contains(got, "Gitee tag v1.0.51 has no complete release assets; using v1.0.49") {
|
||||||
|
t.Fatalf("install output missing incomplete-release fallback:\n%s", got)
|
||||||
|
}
|
||||||
|
installed := filepath.Join(installDir, "dws")
|
||||||
|
versionOutput, err := exec.Command(installed).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("installed dws error = %v\noutput:\n%s", err, string(versionOutput))
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(versionOutput), "Version: v1.0.49") {
|
||||||
|
t.Fatalf("installed version output = %q, want v1.0.49", string(versionOutput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstallersRequireCompleteGiteeReleaseAssets(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
checks := []struct {
|
||||||
|
relPath string
|
||||||
|
wants []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
relPath: filepath.Join("..", "..", "scripts", "install.sh"),
|
||||||
|
wants: []string{"resolve_gitee_latest_version", `resolve_version "$archive_name" checksums.txt dws-skills.zip`, `resolve_version "$skills_archive"`},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relPath: filepath.Join("..", "..", "scripts", "install-skills.sh"),
|
||||||
|
wants: []string{"resolve_gitee_latest_version", "resolve_version dws-skills.zip"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relPath: filepath.Join("..", "..", "scripts", "install.ps1"),
|
||||||
|
wants: []string{"Test-GiteeReleaseAssets", `$requiredAssets += "dws-skills.zip"`, `Resolve-LatestVersion @("dws-skills.zip")`},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range checks {
|
||||||
|
data, err := os.ReadFile(tc.relPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(%s) error = %v", tc.relPath, err)
|
||||||
|
}
|
||||||
|
for _, want := range tc.wants {
|
||||||
|
if !strings.Contains(string(data), want) {
|
||||||
|
t.Fatalf("%s missing %q", tc.relPath, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInstallEventScriptStaticExpectations(t *testing.T) {
|
func TestInstallEventScriptStaticExpectations(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -822,6 +922,47 @@ func writeFakeGH(t *testing.T, path, version string) {
|
|||||||
mustWriteFile(t, path, []byte(script), 0o755)
|
mustWriteFile(t, path, []byte(script), 0o755)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeFakeGiteeCurl(t *testing.T, path string) {
|
||||||
|
t.Helper()
|
||||||
|
const script = `#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
out=""
|
||||||
|
url=""
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-o) out="$2"; shift 2 ;;
|
||||||
|
-*) shift ;;
|
||||||
|
*) url="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
case "$url" in
|
||||||
|
*/tags)
|
||||||
|
printf '%s' '[{"name":"v1.0.49"},{"name":"v1.0.51"},{"name":"v1.0.50"}]'
|
||||||
|
;;
|
||||||
|
*/releases/tags/v1.0.51)
|
||||||
|
printf '%s' 'null'
|
||||||
|
;;
|
||||||
|
*/releases/tags/v1.0.50)
|
||||||
|
printf '%s' '{"tag_name":"v1.0.50","assets":[{"name":"checksums.txt","browser_download_url":"https://assets.invalid/checksums.txt"}]}'
|
||||||
|
;;
|
||||||
|
*/releases/tags/v1.0.49)
|
||||||
|
printf '%s' '{"tag_name":"v1.0.49","assets":[{"name":"dws-linux-amd64.tar.gz","browser_download_url":"https://assets.invalid/dws-linux-amd64.tar.gz"},{"name":"checksums.txt","browser_download_url":"https://assets.invalid/checksums.txt"}]}'
|
||||||
|
;;
|
||||||
|
https://assets.invalid/dws-linux-amd64.tar.gz)
|
||||||
|
cp "$FAKE_RELEASE_DIR/dws-linux-amd64.tar.gz" "$out"
|
||||||
|
;;
|
||||||
|
https://assets.invalid/checksums.txt)
|
||||||
|
cp "$FAKE_RELEASE_DIR/checksums.txt" "$out"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "fake curl: unexpected URL $url" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
`
|
||||||
|
mustWriteFile(t, path, []byte(script), 0o755)
|
||||||
|
}
|
||||||
|
|
||||||
func mustWriteFile(t *testing.T, path string, data []byte, mode os.FileMode) {
|
func mustWriteFile(t *testing.T, path string, data []byte, mode os.FileMode) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user