Merge pull request #1519 from trheyi/main
feat(workflows): enhance notarization and release processes for macOS…
This commit is contained in:
commit
ccedc28828
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
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Release"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_id:
|
||||
description: "Release macOS workflow run ID (to download artifacts from)"
|
||||
required: true
|
||||
version:
|
||||
description: "Version used in the release build (e.g. 1.0.0 or 1.0.0-alpha)"
|
||||
required: true
|
||||
description: "Version (auto-detected from latest release if empty)"
|
||||
required: false
|
||||
run_id:
|
||||
description: "Release macOS workflow run ID (auto-detected if empty)"
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
|
||||
concurrency:
|
||||
group: notarize-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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:
|
||||
needs: resolve
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
|
@ -28,7 +84,7 @@ jobs:
|
|||
with:
|
||||
name: yao-darwin-${{ matrix.arch }}
|
||||
path: bin
|
||||
run-id: ${{ github.event.inputs.run_id }}
|
||||
run-id: ${{ needs.resolve.outputs.run_id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Certificates
|
||||
|
|
@ -86,3 +142,68 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
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
|
||||
container:
|
||||
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:
|
||||
- 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
|
||||
run: |
|
||||
export PATH=$PATH:/github/home/go/bin
|
||||
|
|
@ -72,13 +59,6 @@ jobs:
|
|||
mv /app/yao/dist/release/* /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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
|
|
|||
40
.github/workflows/release.yml
vendored
40
.github/workflows/release.yml
vendored
|
|
@ -87,8 +87,13 @@ jobs:
|
|||
VERSION="${{ steps.version.outputs.version }}"
|
||||
mkdir -p release
|
||||
|
||||
# Linux artifacts (already named correctly from build.sh)
|
||||
cp dist/linux/* release/ 2>/dev/null || true
|
||||
# Linux prod binaries (canonical name = prod/stripped)
|
||||
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
|
||||
cp dist/macos/arm64-prod/yao "release/yao-${VERSION}-darwin-arm64"
|
||||
|
|
@ -112,3 +117,34 @@ jobs:
|
|||
name: Yao v${{ steps.version.outputs.version }}
|
||||
files: release/*
|
||||
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✨": "✨服务已停止✨",
|
||||
"SessionPort": "会话服务端口",
|
||||
"Force migrate": "强制更新数据表结构",
|
||||
"Migrate is not allowed on production mode.": "Migrate 不能再生产环境下使用",
|
||||
"Upgrade yao to latest version": "升级 yao 到最新版本",
|
||||
"Current version:": "当前版本:",
|
||||
"Latest version: ": "最新版本: ",
|
||||
"Checking latest version...": "正在检查最新版本...",
|
||||
"🎉Current version is the latest🎉": "🎉当前版本是最新的🎉",
|
||||
"Do you want to update to %s ? (y/n): ": "是否更新到 %s ? (y/n): ",
|
||||
"Invalid input": "输入错误",
|
||||
"Canceled upgrade": "已取消更新",
|
||||
"Downloading...": "正在下载...",
|
||||
"Progress:": "进度:",
|
||||
"Available assets:": "可用的制品:",
|
||||
"Error occurred while updating binary: %s": "更新二进制文件时出错: %s",
|
||||
"🎉Successfully updated to version: %s🎉": "🎉成功更新到版本: %s🎉",
|
||||
"Print all version information": "显示详细版本信息",
|
||||
"SUI Template Engine": "SUI 模板引擎命令",
|
||||
"MCP commands": "MCP 包管理命令",
|
||||
"MCP package management commands": "MCP 包管理命令",
|
||||
"Robot commands": "Robot 包管理命令",
|
||||
"Robot package management commands": "Robot 包管理命令",
|
||||
"Migrate is not allowed on production mode.": "Migrate 不能再生产环境下使用",
|
||||
"Upgrade yao to latest version": "升级 yao 到最新版本",
|
||||
"Current version:": "当前版本:",
|
||||
"Latest version: ": "最新版本: ",
|
||||
"Checking latest version...": "正在检查最新版本...",
|
||||
"🎉Current version is the latest🎉": "🎉当前版本是最新的🎉",
|
||||
"Do you want to update to %s ? (y/n): ": "是否更新到 %s ? (y/n): ",
|
||||
"Invalid input": "输入错误",
|
||||
"Canceled upgrade": "已取消更新",
|
||||
"Downloading...": "正在下载...",
|
||||
"Progress:": "进度:",
|
||||
"Available assets:": "可用的制品:",
|
||||
"Error occurred while updating binary: %s": "更新二进制文件时出错: %s",
|
||||
"🎉Successfully updated to version: %s🎉": "🎉成功更新到版本: %s🎉",
|
||||
"Skip interactive confirmation": "跳过交互式确认",
|
||||
"Only check for updates and print JSON result": "仅检查更新并输出 JSON 结果",
|
||||
"Custom download source URL (e.g. https://get.yaoapps.com/releases/yao)": "自定义下载源 URL (例如 https://get.yaoapps.com/releases/yao)",
|
||||
"Print all version information": "显示详细版本信息",
|
||||
"SUI Template Engine": "SUI 模板引擎命令",
|
||||
"MCP commands": "MCP 包管理命令",
|
||||
"MCP package management commands": "MCP 包管理命令",
|
||||
"Robot commands": "Robot 包管理命令",
|
||||
"Robot package management commands": "Robot 包管理命令",
|
||||
}
|
||||
|
||||
// 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 cdnFallbackBase = "https://get.yaoapps.com/releases/yao"
|
||||
|
||||
// Upgrade command flags
|
||||
var (
|
||||
upgradeYes bool
|
||||
upgradeCheck bool
|
||||
upgradeSource string
|
||||
)
|
||||
|
||||
// githubRelease represents a GitHub release response
|
||||
type githubRelease struct {
|
||||
|
|
@ -36,55 +44,87 @@ type githubAsset struct {
|
|||
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{
|
||||
Use: "upgrade",
|
||||
Short: L("Upgrade yao to latest version"),
|
||||
Long: L("Upgrade yao to latest version"),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("%s %s\n", color.WhiteString(L("Current version:")), color.CyanString(share.VERSION))
|
||||
fmt.Println(color.WhiteString(L("Checking latest version...")))
|
||||
|
||||
release, err := fetchLatestRelease()
|
||||
latestVersion, downloadURL, err := resolveLatest()
|
||||
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()))
|
||||
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))
|
||||
|
||||
if latestVersion == share.VERSION {
|
||||
if !updateAvailable {
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
fmt.Println(color.RedString(L("Fatal: %s"), err.Error()))
|
||||
|
|
@ -96,8 +136,8 @@ var upgradeCmd = &cobra.Command{
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s\n", color.WhiteString(L("Downloading...")), color.CyanString(asset.BrowserDownloadURL))
|
||||
if err := downloadAndReplace(asset.BrowserDownloadURL, exe); err != nil {
|
||||
fmt.Printf("%s %s\n", color.WhiteString(L("Downloading...")), color.CyanString(downloadURL))
|
||||
if err := downloadAndReplace(downloadURL, exe); err != nil {
|
||||
fmt.Println(color.RedString(L("Error occurred while updating binary: %s"), err.Error()))
|
||||
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
|
||||
func fetchLatestRelease() (*githubRelease, error) {
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
|
|
@ -256,3 +434,9 @@ func copyFile(src, dst string) error {
|
|||
_, err = io.Copy(out, in)
|
||||
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