feat(workflows): enhance notarization and release processes for macOS and Linux
- Updated the notarization workflow to automatically resolve version and run ID from the latest release or inputs. - Added a finalize job to verify assets on R2 and trigger CDN updates after notarization. - Improved the release workflow to upload binaries to R2 with enhanced error handling and asset verification. - Refactored upgrade command to support CDN fallback for version resolution, improving update reliability. - Enhanced localization in the command output for better user experience.
This commit is contained in:
parent
dd9c81068b
commit
f7ee21372f
6 changed files with 527 additions and 83 deletions
133
.github/workflows/notarize-macos.yml
vendored
133
.github/workflows/notarize-macos.yml
vendored
|
|
@ -1,23 +1,79 @@
|
||||||
name: Notarize macOS
|
name: Notarize macOS
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Release"]
|
||||||
|
types: [completed]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
run_id:
|
|
||||||
description: "Release macOS workflow run ID (to download artifacts from)"
|
|
||||||
required: true
|
|
||||||
version:
|
version:
|
||||||
description: "Version used in the release build (e.g. 1.0.0 or 1.0.0-alpha)"
|
description: "Version (auto-detected from latest release if empty)"
|
||||||
required: true
|
required: false
|
||||||
|
run_id:
|
||||||
|
description: "Release macOS workflow run ID (auto-detected if empty)"
|
||||||
|
required: false
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: notarize-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# ===================================================================
|
||||||
|
# Resolve version + macOS build run_id automatically
|
||||||
|
# ===================================================================
|
||||||
|
resolve:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
startsWith(github.event.workflow_run.head_branch, 'v'))
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.resolve.outputs.version }}
|
||||||
|
run_id: ${{ steps.resolve.outputs.run_id }}
|
||||||
|
steps:
|
||||||
|
- name: Resolve version and run_id
|
||||||
|
id: resolve
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
|
RUN_ID="${{ github.event.inputs.run_id }}"
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
TAG=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q '.tagName')
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
fi
|
||||||
|
if [ -z "$RUN_ID" ]; then
|
||||||
|
RUN_ID=$(gh run list --repo "$GITHUB_REPOSITORY" \
|
||||||
|
--workflow="Release macOS" --branch="v${VERSION}" --limit=1 \
|
||||||
|
--json databaseId --jq '.[0].databaseId')
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
TAG="${{ github.event.workflow_run.head_branch }}"
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
RUN_ID=$(gh run list --repo "$GITHUB_REPOSITORY" \
|
||||||
|
--workflow="Release macOS" --branch="${TAG}" --limit=1 \
|
||||||
|
--json databaseId --jq '.[0].databaseId')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ] || [ -z "$RUN_ID" ]; then
|
||||||
|
echo "::error::Failed to resolve version='${VERSION}' run_id='${RUN_ID}'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
echo "run_id=${RUN_ID}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Resolved: version=${VERSION} run_id=${RUN_ID}"
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# Notarize Yao binaries (arm64 + amd64)
|
# Notarize Yao binaries (arm64 + amd64)
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
notarize:
|
notarize:
|
||||||
|
needs: resolve
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
|
@ -28,7 +84,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: yao-darwin-${{ matrix.arch }}
|
name: yao-darwin-${{ matrix.arch }}
|
||||||
path: bin
|
path: bin
|
||||||
run-id: ${{ github.event.inputs.run_id }}
|
run-id: ${{ needs.resolve.outputs.run_id }}
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Install Certificates
|
- name: Install Certificates
|
||||||
|
|
@ -86,3 +142,68 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Yao ${{ matrix.arch }} notarization accepted."
|
echo "Yao ${{ matrix.arch }} notarization accepted."
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# After both architectures finish: verify R2 assets + trigger CDN
|
||||||
|
# ===================================================================
|
||||||
|
finalize:
|
||||||
|
needs: [resolve, notarize]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: success()
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_ENDPOINTS: ${{ secrets.R2_ENDPOINTS }}
|
||||||
|
R2_BUCKET: ${{ secrets.R2_BUCKET || 'get-yaoapps' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout (for gh CLI context)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: .github
|
||||||
|
|
||||||
|
- name: Configure AWS CLI
|
||||||
|
run: |
|
||||||
|
aws configure set default.region us-east-1
|
||||||
|
aws configure set default.s3.signature_version s3v4
|
||||||
|
|
||||||
|
- name: Verify all platform assets on R2
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.resolve.outputs.version }}"
|
||||||
|
PREFIX="releases/yao/${VERSION}"
|
||||||
|
|
||||||
|
PLATFORMS=(
|
||||||
|
"darwin-arm64"
|
||||||
|
"darwin-amd64"
|
||||||
|
"linux-amd64"
|
||||||
|
"linux-arm64"
|
||||||
|
)
|
||||||
|
|
||||||
|
MISSING=0
|
||||||
|
for P in "${PLATFORMS[@]}"; do
|
||||||
|
KEY="${PREFIX}/yao-${VERSION}-${P}"
|
||||||
|
if ! aws s3 ls "s3://${R2_BUCKET}/${KEY}" --endpoint-url "$R2_ENDPOINTS" >/dev/null 2>&1; then
|
||||||
|
echo "::warning::Missing asset: ${KEY}"
|
||||||
|
MISSING=$((MISSING+1))
|
||||||
|
fi
|
||||||
|
if ! aws s3 ls "s3://${R2_BUCKET}/${KEY}.sha256" --endpoint-url "$R2_ENDPOINTS" >/dev/null 2>&1; then
|
||||||
|
echo "::warning::Missing sha256: ${KEY}.sha256"
|
||||||
|
MISSING=$((MISSING+1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$MISSING" -gt 0 ]; then
|
||||||
|
echo "::error::$MISSING required asset(s) missing on R2."
|
||||||
|
echo "Ensure release.yml has completed successfully before running notarize."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "All 4 platform assets verified on R2."
|
||||||
|
|
||||||
|
- name: Trigger CDN latest.json update
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.resolve.outputs.version }}"
|
||||||
|
gh workflow run update-cdn-latest.yml \
|
||||||
|
-f version="${VERSION}" \
|
||||||
|
-f mark_latest="true"
|
||||||
|
echo "Triggered update-cdn-latest.yml for ${VERSION}"
|
||||||
|
|
|
||||||
20
.github/workflows/release-linux.yml
vendored
20
.github/workflows/release-linux.yml
vendored
|
|
@ -20,20 +20,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: yaoapp/yao-build:1.0.0
|
image: yaoapp/yao-build:1.0.0
|
||||||
env:
|
|
||||||
CF_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
|
|
||||||
CF_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
|
||||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
|
||||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Configure R2 For Cloudflare
|
|
||||||
run: |
|
|
||||||
aws configure set aws_access_key_id $CF_ACCESS_KEY_ID
|
|
||||||
aws configure set aws_secret_access_key $CF_SECRET_ACCESS_KEY
|
|
||||||
aws configure set default.region us-east-1
|
|
||||||
aws configure set default.s3.signature_version s3v4
|
|
||||||
aws configure set default.s3.endpoint_url https://$R2_ACCOUNT_ID.r2.cloudflarestorage.com
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
export PATH=$PATH:/github/home/go/bin
|
export PATH=$PATH:/github/home/go/bin
|
||||||
|
|
@ -72,13 +59,6 @@ jobs:
|
||||||
mv /app/yao/dist/release/* /data/
|
mv /app/yao/dist/release/* /data/
|
||||||
ls -l /data
|
ls -l /data
|
||||||
|
|
||||||
- name: Push To R2
|
|
||||||
run: |
|
|
||||||
for file in /data/*; do
|
|
||||||
aws s3 cp "$file" s3://$R2_BUCKET/archives/ \
|
|
||||||
--endpoint-url https://$R2_ACCOUNT_ID.r2.cloudflarestorage.com
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
40
.github/workflows/release.yml
vendored
40
.github/workflows/release.yml
vendored
|
|
@ -87,8 +87,13 @@ jobs:
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
mkdir -p release
|
mkdir -p release
|
||||||
|
|
||||||
# Linux artifacts (already named correctly from build.sh)
|
# Linux prod binaries (canonical name = prod/stripped)
|
||||||
cp dist/linux/* release/ 2>/dev/null || true
|
cp "dist/linux/yao-${VERSION}-linux-amd64-prod" "release/yao-${VERSION}-linux-amd64"
|
||||||
|
cp "dist/linux/yao-${VERSION}-linux-arm64-prod" "release/yao-${VERSION}-linux-arm64"
|
||||||
|
|
||||||
|
# Linux dev binaries
|
||||||
|
cp "dist/linux/yao-${VERSION}-linux-amd64" "release/yao-${VERSION}-linux-amd64-dev"
|
||||||
|
cp "dist/linux/yao-${VERSION}-linux-arm64" "release/yao-${VERSION}-linux-arm64-dev"
|
||||||
|
|
||||||
# macOS prod binaries
|
# macOS prod binaries
|
||||||
cp dist/macos/arm64-prod/yao "release/yao-${VERSION}-darwin-arm64"
|
cp dist/macos/arm64-prod/yao "release/yao-${VERSION}-darwin-arm64"
|
||||||
|
|
@ -112,3 +117,34 @@ jobs:
|
||||||
name: Yao v${{ steps.version.outputs.version }}
|
name: Yao v${{ steps.version.outputs.version }}
|
||||||
files: release/*
|
files: release/*
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|
||||||
|
- name: Upload all binaries to R2
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_ENDPOINTS: ${{ secrets.R2_ENDPOINTS }}
|
||||||
|
R2_BUCKET: ${{ secrets.R2_BUCKET || 'get-yaoapps' }}
|
||||||
|
run: |
|
||||||
|
aws configure set default.region us-east-1
|
||||||
|
aws configure set default.s3.signature_version s3v4
|
||||||
|
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
PREFIX="releases/yao/${VERSION}"
|
||||||
|
|
||||||
|
echo "Uploading release binaries to R2: ${PREFIX}"
|
||||||
|
|
||||||
|
for file in release/yao-${VERSION}-*; do
|
||||||
|
name=$(basename "$file")
|
||||||
|
case "$name" in *-dev|*-prod|*.sha256) continue ;; esac
|
||||||
|
|
||||||
|
sha256sum "$file" | awk '{print $1}' > "/tmp/${name}.sha256"
|
||||||
|
|
||||||
|
aws s3 cp "$file" "s3://${R2_BUCKET}/${PREFIX}/${name}" \
|
||||||
|
--endpoint-url "$R2_ENDPOINTS" \
|
||||||
|
--content-type "application/octet-stream"
|
||||||
|
aws s3 cp "/tmp/${name}.sha256" "s3://${R2_BUCKET}/${PREFIX}/${name}.sha256" \
|
||||||
|
--endpoint-url "$R2_ENDPOINTS" \
|
||||||
|
--content-type "text/plain"
|
||||||
|
|
||||||
|
echo "Uploaded: ${name} + ${name}.sha256"
|
||||||
|
done
|
||||||
|
|
|
||||||
120
.github/workflows/update-cdn-latest.yml
vendored
Normal file
120
.github/workflows/update-cdn-latest.yml
vendored
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
name: Update CDN latest.json
|
||||||
|
|
||||||
|
# Assembles releases/yao/latest.json after all platform binaries are on R2.
|
||||||
|
#
|
||||||
|
# Normally triggered automatically by notarize-macos.yml's finalize job after
|
||||||
|
# notarization completes. Can also be triggered manually as a fallback.
|
||||||
|
#
|
||||||
|
# Prerequisites: release.yml must have uploaded all 4 platform binaries to R2.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Engine version to publish (e.g. 1.0.0 or 1.0.0-alpha)"
|
||||||
|
required: true
|
||||||
|
mark_latest:
|
||||||
|
description: "Also update releases/yao/latest.json (set false for pre-releases you want on CDN but not as latest)"
|
||||||
|
required: false
|
||||||
|
default: "true"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-latest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_ENDPOINTS: ${{ secrets.R2_ENDPOINTS }}
|
||||||
|
R2_BUCKET: ${{ secrets.R2_BUCKET || 'get-yaoapps' }}
|
||||||
|
CDN_BASE: https://get.yaoapps.com
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS CLI
|
||||||
|
run: |
|
||||||
|
aws configure set default.region us-east-1
|
||||||
|
aws configure set default.s3.signature_version s3v4
|
||||||
|
|
||||||
|
- name: Verify platform assets exist
|
||||||
|
run: |
|
||||||
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
|
PREFIX="releases/yao/${VERSION}"
|
||||||
|
|
||||||
|
PLATFORMS=(
|
||||||
|
"darwin-arm64"
|
||||||
|
"darwin-amd64"
|
||||||
|
"linux-amd64"
|
||||||
|
"linux-arm64"
|
||||||
|
)
|
||||||
|
|
||||||
|
MISSING=0
|
||||||
|
for P in "${PLATFORMS[@]}"; do
|
||||||
|
KEY="${PREFIX}/yao-${VERSION}-${P}"
|
||||||
|
echo "Checking s3://${R2_BUCKET}/${KEY}"
|
||||||
|
if ! aws s3 ls "s3://${R2_BUCKET}/${KEY}" --endpoint-url "$R2_ENDPOINTS" >/dev/null 2>&1; then
|
||||||
|
echo "::warning::Missing asset: ${KEY}"
|
||||||
|
MISSING=$((MISSING+1))
|
||||||
|
fi
|
||||||
|
if ! aws s3 ls "s3://${R2_BUCKET}/${KEY}.sha256" --endpoint-url "$R2_ENDPOINTS" >/dev/null 2>&1; then
|
||||||
|
echo "::warning::Missing sha256: ${KEY}.sha256"
|
||||||
|
MISSING=$((MISSING+1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$MISSING" -gt 0 ]; then
|
||||||
|
echo "::error::$MISSING required asset(s) are missing on R2. Run platform CI workflows first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "All platform assets verified."
|
||||||
|
|
||||||
|
- name: Build latest.json
|
||||||
|
run: |
|
||||||
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
|
RELEASED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
|
||||||
|
python3 <<PY > /tmp/latest.json
|
||||||
|
import json
|
||||||
|
version = "${VERSION}"
|
||||||
|
base = "${CDN_BASE}/releases/yao/${VERSION}"
|
||||||
|
assets = {
|
||||||
|
"darwin-arm64": f"{base}/yao-{version}-darwin-arm64",
|
||||||
|
"darwin-amd64": f"{base}/yao-{version}-darwin-amd64",
|
||||||
|
"linux-amd64": f"{base}/yao-{version}-linux-amd64",
|
||||||
|
"linux-arm64": f"{base}/yao-{version}-linux-arm64",
|
||||||
|
}
|
||||||
|
sha256 = {
|
||||||
|
"darwin-arm64": f"{base}/yao-{version}-darwin-arm64.sha256",
|
||||||
|
"darwin-amd64": f"{base}/yao-{version}-darwin-amd64.sha256",
|
||||||
|
"linux-amd64": f"{base}/yao-{version}-linux-amd64.sha256",
|
||||||
|
"linux-arm64": f"{base}/yao-{version}-linux-arm64.sha256",
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"version": version,
|
||||||
|
"released_at": "${RELEASED_AT}",
|
||||||
|
"assets": assets,
|
||||||
|
"sha256": sha256,
|
||||||
|
}
|
||||||
|
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||||
|
PY
|
||||||
|
|
||||||
|
cat /tmp/latest.json
|
||||||
|
|
||||||
|
- name: Upload versioned latest.json
|
||||||
|
run: |
|
||||||
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
|
aws s3 cp /tmp/latest.json \
|
||||||
|
"s3://${R2_BUCKET}/releases/yao/${VERSION}/latest.json" \
|
||||||
|
--endpoint-url "$R2_ENDPOINTS" \
|
||||||
|
--content-type "application/json" \
|
||||||
|
--cache-control "public, max-age=60"
|
||||||
|
|
||||||
|
- name: Promote to releases/yao/latest.json
|
||||||
|
if: ${{ github.event.inputs.mark_latest != 'false' }}
|
||||||
|
run: |
|
||||||
|
aws s3 cp /tmp/latest.json \
|
||||||
|
"s3://${R2_BUCKET}/releases/yao/latest.json" \
|
||||||
|
--endpoint-url "$R2_ENDPOINTS" \
|
||||||
|
--content-type "application/json" \
|
||||||
|
--cache-control "public, max-age=60"
|
||||||
|
echo "Promoted to releases/yao/latest.json"
|
||||||
43
cmd/root.go
43
cmd/root.go
|
|
@ -56,26 +56,29 @@ var langs = map[string]string{
|
||||||
"✨STOPPED✨": "✨服务已停止✨",
|
"✨STOPPED✨": "✨服务已停止✨",
|
||||||
"SessionPort": "会话服务端口",
|
"SessionPort": "会话服务端口",
|
||||||
"Force migrate": "强制更新数据表结构",
|
"Force migrate": "强制更新数据表结构",
|
||||||
"Migrate is not allowed on production mode.": "Migrate 不能再生产环境下使用",
|
"Migrate is not allowed on production mode.": "Migrate 不能再生产环境下使用",
|
||||||
"Upgrade yao to latest version": "升级 yao 到最新版本",
|
"Upgrade yao to latest version": "升级 yao 到最新版本",
|
||||||
"Current version:": "当前版本:",
|
"Current version:": "当前版本:",
|
||||||
"Latest version: ": "最新版本: ",
|
"Latest version: ": "最新版本: ",
|
||||||
"Checking latest version...": "正在检查最新版本...",
|
"Checking latest version...": "正在检查最新版本...",
|
||||||
"🎉Current version is the latest🎉": "🎉当前版本是最新的🎉",
|
"🎉Current version is the latest🎉": "🎉当前版本是最新的🎉",
|
||||||
"Do you want to update to %s ? (y/n): ": "是否更新到 %s ? (y/n): ",
|
"Do you want to update to %s ? (y/n): ": "是否更新到 %s ? (y/n): ",
|
||||||
"Invalid input": "输入错误",
|
"Invalid input": "输入错误",
|
||||||
"Canceled upgrade": "已取消更新",
|
"Canceled upgrade": "已取消更新",
|
||||||
"Downloading...": "正在下载...",
|
"Downloading...": "正在下载...",
|
||||||
"Progress:": "进度:",
|
"Progress:": "进度:",
|
||||||
"Available assets:": "可用的制品:",
|
"Available assets:": "可用的制品:",
|
||||||
"Error occurred while updating binary: %s": "更新二进制文件时出错: %s",
|
"Error occurred while updating binary: %s": "更新二进制文件时出错: %s",
|
||||||
"🎉Successfully updated to version: %s🎉": "🎉成功更新到版本: %s🎉",
|
"🎉Successfully updated to version: %s🎉": "🎉成功更新到版本: %s🎉",
|
||||||
"Print all version information": "显示详细版本信息",
|
"Skip interactive confirmation": "跳过交互式确认",
|
||||||
"SUI Template Engine": "SUI 模板引擎命令",
|
"Only check for updates and print JSON result": "仅检查更新并输出 JSON 结果",
|
||||||
"MCP commands": "MCP 包管理命令",
|
"Custom download source URL (e.g. https://get.yaoapps.com/releases/yao)": "自定义下载源 URL (例如 https://get.yaoapps.com/releases/yao)",
|
||||||
"MCP package management commands": "MCP 包管理命令",
|
"Print all version information": "显示详细版本信息",
|
||||||
"Robot commands": "Robot 包管理命令",
|
"SUI Template Engine": "SUI 模板引擎命令",
|
||||||
"Robot package management commands": "Robot 包管理命令",
|
"MCP commands": "MCP 包管理命令",
|
||||||
|
"MCP package management commands": "MCP 包管理命令",
|
||||||
|
"Robot commands": "Robot 包管理命令",
|
||||||
|
"Robot package management commands": "Robot 包管理命令",
|
||||||
}
|
}
|
||||||
|
|
||||||
// L Language switch
|
// L Language switch
|
||||||
|
|
|
||||||
254
cmd/upgrade.go
254
cmd/upgrade.go
|
|
@ -18,6 +18,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const githubReleasesAPI = "https://api.github.com/repos/YaoApp/yao/releases/latest"
|
const githubReleasesAPI = "https://api.github.com/repos/YaoApp/yao/releases/latest"
|
||||||
|
const cdnFallbackBase = "https://get.yaoapps.com/releases/yao"
|
||||||
|
|
||||||
|
// Upgrade command flags
|
||||||
|
var (
|
||||||
|
upgradeYes bool
|
||||||
|
upgradeCheck bool
|
||||||
|
upgradeSource string
|
||||||
|
)
|
||||||
|
|
||||||
// githubRelease represents a GitHub release response
|
// githubRelease represents a GitHub release response
|
||||||
type githubRelease struct {
|
type githubRelease struct {
|
||||||
|
|
@ -36,55 +44,87 @@ type githubAsset struct {
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cdnLatest represents CDN latest.json format
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "version": "1.0.0",
|
||||||
|
// "released_at": "...",
|
||||||
|
// "assets": { "darwin-arm64": "https://...", ... }
|
||||||
|
// }
|
||||||
|
type cdnLatest struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
ReleasedAt string `json:"released_at"`
|
||||||
|
Assets map[string]string `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkResult is the JSON payload emitted by `yao upgrade --check`
|
||||||
|
type checkResult struct {
|
||||||
|
Current string `json:"current"`
|
||||||
|
Latest string `json:"latest"`
|
||||||
|
UpdateAvailable bool `json:"update_available"`
|
||||||
|
DownloadURL string `json:"download_url,omitempty"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
}
|
||||||
|
|
||||||
var upgradeCmd = &cobra.Command{
|
var upgradeCmd = &cobra.Command{
|
||||||
Use: "upgrade",
|
Use: "upgrade",
|
||||||
Short: L("Upgrade yao to latest version"),
|
Short: L("Upgrade yao to latest version"),
|
||||||
Long: L("Upgrade yao to latest version"),
|
Long: L("Upgrade yao to latest version"),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
fmt.Printf("%s %s\n", color.WhiteString(L("Current version:")), color.CyanString(share.VERSION))
|
latestVersion, downloadURL, err := resolveLatest()
|
||||||
fmt.Println(color.WhiteString(L("Checking latest version...")))
|
|
||||||
|
|
||||||
release, err := fetchLatestRelease()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if upgradeCheck {
|
||||||
|
emitCheckJSON(checkResult{
|
||||||
|
Current: share.VERSION,
|
||||||
|
Source: upgradeSource,
|
||||||
|
}, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
fmt.Println(color.RedString(L("Fatal: %s"), err.Error()))
|
fmt.Println(color.RedString(L("Fatal: %s"), err.Error()))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
latestVersion := strings.TrimPrefix(release.TagName, "v")
|
updateAvailable := compareVersions(latestVersion, share.VERSION) > 0
|
||||||
|
|
||||||
|
if upgradeCheck {
|
||||||
|
emitCheckJSON(checkResult{
|
||||||
|
Current: share.VERSION,
|
||||||
|
Latest: latestVersion,
|
||||||
|
UpdateAvailable: updateAvailable,
|
||||||
|
DownloadURL: downloadURL,
|
||||||
|
Source: upgradeSource,
|
||||||
|
}, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s %s\n", color.WhiteString(L("Current version:")), color.CyanString(share.VERSION))
|
||||||
fmt.Printf("%s %s\n", color.WhiteString(L("Latest version: ")), color.GreenString(latestVersion))
|
fmt.Printf("%s %s\n", color.WhiteString(L("Latest version: ")), color.GreenString(latestVersion))
|
||||||
|
|
||||||
if latestVersion == share.VERSION {
|
if !updateAvailable {
|
||||||
fmt.Println(color.GreenString(L("🎉Current version is the latest🎉")))
|
fmt.Println(color.GreenString(L("🎉Current version is the latest🎉")))
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
assetName := buildAssetName(latestVersion)
|
|
||||||
asset := findAsset(release.Assets, assetName)
|
|
||||||
if asset == nil {
|
|
||||||
fmt.Println(color.RedString(L("Fatal: %s"), fmt.Sprintf("asset not found: %s", assetName)))
|
|
||||||
fmt.Printf("%s %s\n", color.WhiteString(L("Available assets:")), "")
|
|
||||||
for _, a := range release.Assets {
|
|
||||||
if !strings.HasSuffix(a.Name, ".sha256") && !strings.HasSuffix(a.Name, ".zip") && !strings.HasSuffix(a.Name, ".tar.gz") {
|
|
||||||
fmt.Printf(" - %s\n", color.YellowString(a.Name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", color.WhiteString(L("Do you want to update to %s ? (y/n): "), latestVersion), "")
|
|
||||||
fmt.Print("> ")
|
|
||||||
input, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(color.RedString(L("Fatal: %s"), err.Error()))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
input = strings.TrimSpace(input)
|
|
||||||
if input != "y" && input != "Y" {
|
|
||||||
fmt.Println(color.YellowString(L("Canceled upgrade")))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if downloadURL == "" {
|
||||||
|
fmt.Println(color.RedString(L("Fatal: %s"), fmt.Sprintf("asset not found for %s-%s", runtime.GOOS, runtime.GOARCH)))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !upgradeYes {
|
||||||
|
fmt.Printf("%s %s\n", color.WhiteString(L("Do you want to update to %s ? (y/n): "), latestVersion), "")
|
||||||
|
fmt.Print("> ")
|
||||||
|
input, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(color.RedString(L("Fatal: %s"), err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input != "y" && input != "Y" {
|
||||||
|
fmt.Println(color.YellowString(L("Canceled upgrade")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exe, err := os.Executable()
|
exe, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(color.RedString(L("Fatal: %s"), err.Error()))
|
fmt.Println(color.RedString(L("Fatal: %s"), err.Error()))
|
||||||
|
|
@ -96,8 +136,8 @@ var upgradeCmd = &cobra.Command{
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", color.WhiteString(L("Downloading...")), color.CyanString(asset.BrowserDownloadURL))
|
fmt.Printf("%s %s\n", color.WhiteString(L("Downloading...")), color.CyanString(downloadURL))
|
||||||
if err := downloadAndReplace(asset.BrowserDownloadURL, exe); err != nil {
|
if err := downloadAndReplace(downloadURL, exe); err != nil {
|
||||||
fmt.Println(color.RedString(L("Error occurred while updating binary: %s"), err.Error()))
|
fmt.Println(color.RedString(L("Error occurred while updating binary: %s"), err.Error()))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +146,144 @@ var upgradeCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveLatest fetches latest version info. Priority:
|
||||||
|
// 1. --source flag (explicit CDN URL)
|
||||||
|
// 2. GitHub Releases API (default)
|
||||||
|
// 3. Fallback to get.yaoapps.com CDN if GitHub fails (for users in China)
|
||||||
|
func resolveLatest() (string, string, error) {
|
||||||
|
if upgradeSource != "" {
|
||||||
|
return resolveFromCDN(upgradeSource)
|
||||||
|
}
|
||||||
|
ver, url, err := resolveFromGitHub()
|
||||||
|
if err == nil {
|
||||||
|
return ver, url, nil
|
||||||
|
}
|
||||||
|
fmt.Println(color.YellowString(L("GitHub unavailable (%s), trying CDN fallback..."), err.Error()))
|
||||||
|
return resolveFromCDN(cdnFallbackBase)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveFromCDN fetches latest.json from the given CDN base URL.
|
||||||
|
// The URL should point to the directory containing latest.json,
|
||||||
|
// e.g. "https://get.yaoapps.com/releases/yao".
|
||||||
|
func resolveFromCDN(base string) (string, string, error) {
|
||||||
|
url := strings.TrimRight(base, "/") + "/latest.json"
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", fmt.Sprintf("yao/%s", share.VERSION))
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("fetch CDN latest.json failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", "", fmt.Errorf("CDN returned status %d for %s", resp.StatusCode, url)
|
||||||
|
}
|
||||||
|
var data cdnLatest
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||||
|
return "", "", fmt.Errorf("parse latest.json failed: %w", err)
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
dl := data.Assets[key]
|
||||||
|
return strings.TrimPrefix(data.Version, "v"), dl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveFromGitHub keeps the original GitHub Releases API behavior.
|
||||||
|
func resolveFromGitHub() (string, string, error) {
|
||||||
|
release, err := fetchLatestRelease()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
latestVersion := strings.TrimPrefix(release.TagName, "v")
|
||||||
|
assetName := buildAssetName(latestVersion)
|
||||||
|
asset := findAsset(release.Assets, assetName)
|
||||||
|
if asset == nil {
|
||||||
|
return latestVersion, "", nil
|
||||||
|
}
|
||||||
|
return latestVersion, asset.BrowserDownloadURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareVersions returns >0 if a > b, 0 if equal, <0 if a < b.
|
||||||
|
// Uses dotted numeric comparison; falls back to string comparison for non-numeric parts.
|
||||||
|
func compareVersions(a, b string) int {
|
||||||
|
a = strings.TrimPrefix(a, "v")
|
||||||
|
b = strings.TrimPrefix(b, "v")
|
||||||
|
if a == b {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Separate pre-release suffix (after '-')
|
||||||
|
baseA, preA := splitVersion(a)
|
||||||
|
baseB, preB := splitVersion(b)
|
||||||
|
|
||||||
|
partsA := strings.Split(baseA, ".")
|
||||||
|
partsB := strings.Split(baseB, ".")
|
||||||
|
n := len(partsA)
|
||||||
|
if len(partsB) > n {
|
||||||
|
n = len(partsB)
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
var pa, pb string
|
||||||
|
if i < len(partsA) {
|
||||||
|
pa = partsA[i]
|
||||||
|
}
|
||||||
|
if i < len(partsB) {
|
||||||
|
pb = partsB[i]
|
||||||
|
}
|
||||||
|
if c := compareNumeric(pa, pb); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Base parts equal: release version > pre-release version
|
||||||
|
if preA == "" && preB != "" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if preA != "" && preB == "" {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return strings.Compare(preA, preB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitVersion(v string) (string, string) {
|
||||||
|
if idx := strings.Index(v, "-"); idx >= 0 {
|
||||||
|
return v[:idx], v[idx+1:]
|
||||||
|
}
|
||||||
|
return v, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareNumeric(a, b string) int {
|
||||||
|
var na, nb int
|
||||||
|
_, errA := fmt.Sscanf(a, "%d", &na)
|
||||||
|
_, errB := fmt.Sscanf(b, "%d", &nb)
|
||||||
|
if errA == nil && errB == nil {
|
||||||
|
if na < nb {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if na > nb {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return strings.Compare(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// emitCheckJSON writes the check result as a single-line JSON to stdout.
|
||||||
|
func emitCheckJSON(r checkResult, err error) {
|
||||||
|
if err != nil {
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"current": r.Current,
|
||||||
|
"source": r.Source,
|
||||||
|
"error": err.Error(),
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(payload)
|
||||||
|
fmt.Println(string(b))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(r)
|
||||||
|
fmt.Println(string(b))
|
||||||
|
}
|
||||||
|
|
||||||
// fetchLatestRelease fetches the latest release from GitHub API
|
// fetchLatestRelease fetches the latest release from GitHub API
|
||||||
func fetchLatestRelease() (*githubRelease, error) {
|
func fetchLatestRelease() (*githubRelease, error) {
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|
@ -256,3 +434,9 @@ func copyFile(src, dst string) error {
|
||||||
_, err = io.Copy(out, in)
|
_, err = io.Copy(out, in)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
upgradeCmd.Flags().BoolVarP(&upgradeYes, "yes", "y", false, L("Skip interactive confirmation"))
|
||||||
|
upgradeCmd.Flags().BoolVar(&upgradeCheck, "check", false, L("Only check for updates and print JSON result"))
|
||||||
|
upgradeCmd.Flags().StringVar(&upgradeSource, "source", "", L("Custom download source URL (e.g. https://get.yaoapps.com/releases/yao)"))
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue