v1.1.23: Add COS object storage upload functionality #32
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Build and Publish Release | |
on: | |
push: | |
tags: | |
- 'v*' | |
permissions: | |
contents: write | |
jobs: | |
publish-linux: | |
runs-on: ubuntu-latest | |
timeout-minutes: 30 | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
- name: Setup Node.js | |
uses: actions/setup-node@v4 | |
with: | |
node-version: '20' | |
cache: 'npm' | |
- name: Install dependencies | |
run: npm ci | |
- name: Debug environment | |
run: | | |
echo "GitHub ref: ${{ github.ref }}" | |
echo "Tag name: ${{ github.ref_name }}" | |
echo "Repository: ${{ github.repository }}" | |
echo "Is tag push: ${{ startsWith(github.ref, 'refs/tags/') }}" | |
echo "GITHUB_TOKEN exists: ${{ secrets.GITHUB_TOKEN != '' }}" | |
node --version | |
npm --version | |
- name: Publish Linux (AppImage and deb) | |
run: npm run publish:linux | |
env: | |
GH_TOKEN: ${{ secrets.atriordsa }} | |
DEBUG: electron-builder | |
publish-windows: | |
runs-on: windows-latest | |
timeout-minutes: 30 | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
- name: Setup Node.js | |
uses: actions/setup-node@v4 | |
with: | |
node-version: '20' | |
cache: 'npm' | |
- name: Install dependencies | |
run: npm ci | |
- name: Debug environment | |
run: | | |
echo "GitHub ref: ${{ github.ref }}" | |
echo "Tag name: ${{ github.ref_name }}" | |
echo "Repository: ${{ github.repository }}" | |
echo "Is tag push: ${{ startsWith(github.ref, 'refs/tags/') }}" | |
echo "GITHUB_TOKEN exists: ${{ secrets.GITHUB_TOKEN != '' }}" | |
node --version | |
npm --version | |
- name: Publish Windows | |
run: npm run publish:win | |
env: | |
GH_TOKEN: ${{ secrets.atriordsa }} | |
DEBUG: electron-builder | |
publish-macos: | |
runs-on: macos-latest | |
timeout-minutes: 30 | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
- name: Setup Node.js | |
uses: actions/setup-node@v4 | |
with: | |
node-version: '20' | |
cache: 'npm' | |
- name: Install dependencies | |
run: npm ci | |
- name: Debug environment | |
run: | | |
echo "GitHub ref: ${{ github.ref }}" | |
echo "Tag name: ${{ github.ref_name }}" | |
echo "Repository: ${{ github.repository }}" | |
echo "Is tag push: ${{ startsWith(github.ref, 'refs/tags/') }}" | |
echo "GITHUB_TOKEN exists: ${{ secrets.GITHUB_TOKEN != '' }}" | |
node --version | |
npm --version | |
- name: Publish macOS | |
run: npm run publish:mac | |
env: | |
GH_TOKEN: ${{ secrets.atriordsa }} | |
DEBUG: electron-builder | |
cleanup-release: | |
runs-on: ubuntu-latest | |
needs: [publish-linux, publish-windows, publish-macos] | |
if: always() && (needs.publish-linux.result == 'success' || needs.publish-windows.result == 'success' || needs.publish-macos.result == 'success') | |
permissions: | |
contents: write | |
actions: read | |
steps: | |
- name: Debug GitHub token and permissions | |
run: | | |
echo "🔍 Debugging GitHub environment..." | |
echo "Repository: ${{ github.repository }}" | |
echo "Actor: ${{ github.actor }}" | |
echo "Token exists: ${{ secrets.atriordsa != '' }}" | |
echo "Token length: ${#GITHUB_TOKEN}" | |
# 尝试不同的 API 调用 | |
echo "🧪 Testing different API endpoints..." | |
# 测试基本用户信息 | |
echo "Testing /user endpoint:" | |
curl -s -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user | jq -r '.login // "Failed"' || echo "curl failed" | |
# 测试仓库信息 | |
echo "Testing repository endpoint:" | |
curl -s -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${{ github.repository }} | jq -r '.name // "Failed"' || echo "curl failed" | |
env: | |
GITHUB_TOKEN: ${{ secrets.atriordsa }} | |
- name: Clean unwanted files from release | |
run: | | |
set -e # 启用错误时退出 | |
echo "🧹 Starting release cleanup for tag: ${{ github.ref_name }}" | |
# 直接使用 curl 而不是 gh CLI,因为 gh CLI 可能有权限问题 | |
echo "🔗 Testing GitHub API with curl..." | |
# 验证环境变量 | |
if [ -z "$GITHUB_TOKEN" ]; then | |
echo "❌ GITHUB_TOKEN not set" | |
exit 1 | |
fi | |
# 测试 API 连接 | |
USER_RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user) | |
if ! echo "$USER_RESPONSE" | jq -e '.login' > /dev/null 2>&1; then | |
echo "❌ Cannot authenticate with GitHub API" | |
echo "Response: $USER_RESPONSE" | |
exit 1 | |
fi | |
echo "✅ GitHub API authentication successful" | |
# 等待确保所有上传完成 | |
echo "⏰ Waiting 30 seconds for uploads to complete..." | |
sleep 30 | |
echo "📡 Getting release for tag: ${{ github.ref_name }}" | |
# 首先尝试通过标签获取 release,如果失败则从所有 releases 中查找 | |
RELEASE_RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ | |
https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ github.ref_name }}) | |
# 如果直接通过标签获取失败,则从所有 releases 中查找 | |
if ! echo "$RELEASE_RESPONSE" | jq -e '.id' > /dev/null 2>&1; then | |
echo "⚠️ Direct tag lookup failed, searching in all releases..." | |
echo "Direct response: $RELEASE_RESPONSE" | |
ALL_RELEASES=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ | |
https://api.github.com/repos/${{ github.repository }}/releases) | |
RELEASE_RESPONSE=$(echo "$ALL_RELEASES" | jq ".[] | select(.tag_name == \"${{ github.ref_name }}\")") | |
if ! echo "$RELEASE_RESPONSE" | jq -e '.id' > /dev/null 2>&1; then | |
echo "❌ Could not find release for tag ${{ github.ref_name }}" | |
echo "Available releases:" | |
echo "$ALL_RELEASES" | jq -r '.[] | "- \(.tag_name) (ID: \(.id))"' | head -10 | |
exit 1 | |
fi | |
fi | |
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id') | |
echo "✅ Found release ID: $RELEASE_ID" | |
# 获取 assets | |
echo "📁 Getting release assets..." | |
ASSETS_RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ | |
https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets) | |
if ! echo "$ASSETS_RESPONSE" | jq -e '. | length' > /dev/null 2>&1; then | |
echo "❌ Failed to get assets" | |
echo "Response: $ASSETS_RESPONSE" | |
exit 1 | |
fi | |
echo "📁 Current release assets:" | |
echo "$ASSETS_RESPONSE" | jq -r '.[] | "- \(.name) (ID: \(.id))"' | |
# 查找并删除不需要的文件 | |
echo "🔍 Looking for unwanted assets..." | |
# 删除 .yml, .yaml, .blockmap 文件 | |
echo "Finding .yml, .yaml, .blockmap files..." | |
YML_ASSETS=$(echo "$ASSETS_RESPONSE" | jq -r 'if type == "array" then .[] | select(.name | test("\\.(yml|yaml|blockmap)$")) | .id else empty end' 2>/dev/null || echo "") | |
# 删除不带架构标识的 Windows 文件 | |
echo "Finding Windows files without architecture identifiers..." | |
WINDOWS_NO_ARCH_ASSETS=$(echo "$ASSETS_RESPONSE" | jq -r 'if type == "array" then .[] | select(.name | test("windows.*\\.(exe)$") and (.name | test("_(x64|arm64)_") | not)) | .id else empty end' 2>/dev/null || echo "") | |
# 合并所有要删除的 assets,过滤空行 | |
ALL_UNWANTED_ASSETS="" | |
if [ -n "$YML_ASSETS" ]; then | |
ALL_UNWANTED_ASSETS="$YML_ASSETS" | |
fi | |
if [ -n "$WINDOWS_NO_ARCH_ASSETS" ]; then | |
if [ -n "$ALL_UNWANTED_ASSETS" ]; then | |
ALL_UNWANTED_ASSETS="$ALL_UNWANTED_ASSETS\n$WINDOWS_NO_ARCH_ASSETS" | |
else | |
ALL_UNWANTED_ASSETS="$WINDOWS_NO_ARCH_ASSETS" | |
fi | |
fi | |
if [ -n "$ALL_UNWANTED_ASSETS" ]; then | |
echo "🎯 Found unwanted assets to delete:" | |
# 显示要删除的文件列表 | |
echo "$ASSETS_RESPONSE" | jq -r 'if type == "array" then .[] | select((.name | test("\\.(yml|yaml|blockmap)$")) or (.name | test("windows.*\\.(exe)$") and (.name | test("_(x64|arm64)_") | not))) | "- \(.name) (ID: \(.id))" else empty end' 2>/dev/null || echo "Error displaying file list" | |
echo -e "$ALL_UNWANTED_ASSETS" | while read -r asset_id; do | |
if [ -n "$asset_id" ] && [ "$asset_id" != "null" ] && [ "$asset_id" != "" ]; then | |
ASSET_NAME=$(echo "$ASSETS_RESPONSE" | jq -r "if type == \"array\" then .[] | select(.id == $asset_id) | .name else \"unknown\" end" 2>/dev/null) | |
echo "🗑️ Deleting: $ASSET_NAME (ID: $asset_id)" | |
DELETE_RESPONSE=$(curl -s -w "%{http_code}" -o /dev/null \ | |
-X DELETE \ | |
-H "Authorization: token $GITHUB_TOKEN" \ | |
https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id) | |
if [ "$DELETE_RESPONSE" = "204" ]; then | |
echo "✅ Successfully deleted: $ASSET_NAME" | |
else | |
echo "❌ Failed to delete: $ASSET_NAME (HTTP: $DELETE_RESPONSE)" | |
fi | |
fi | |
done | |
else | |
echo "🎉 No unwanted files found!" | |
fi | |
echo "🏁 Cleanup completed successfully!" | |
env: | |
GITHUB_TOKEN: ${{ secrets.atriordsa }} | |
upload-to-cos: | |
runs-on: ubuntu-latest | |
needs: [cleanup-release] | |
if: always() && needs.cleanup-release.result == 'success' | |
steps: | |
- name: Setup Python for COS upload | |
uses: actions/setup-python@v4 | |
with: | |
python-version: '3.x' | |
- name: Install COS SDK | |
run: | | |
pip install cos-python-sdk-v5 requests | |
- name: Upload release assets to COS | |
run: | | |
cat > upload_to_cos.py << 'EOF' | |
import os | |
import sys | |
import requests | |
import json | |
from qcloud_cos import CosConfig | |
from qcloud_cos import CosS3Client | |
from urllib.parse import urlparse | |
import time | |
def main(): | |
# 获取环境变量 | |
github_token = os.getenv('GITHUB_TOKEN') | |
cos_secret_id = os.getenv('COS_SECRET_ID') | |
cos_secret_key = os.getenv('COS_SECRET_KEY') | |
bucket = os.getenv('BUCKET') | |
region = os.getenv('REGION') | |
cdn_url = os.getenv('CDN_URL') | |
tag_name = os.getenv('TAG_NAME') | |
repository = os.getenv('REPOSITORY') | |
print(f"🚀 Starting COS upload for release: {tag_name}") | |
# 初始化 COS 客户端 | |
config = CosConfig(Region=region, SecretId=cos_secret_id, SecretKey=cos_secret_key) | |
client = CosS3Client(config) | |
# 获取 GitHub release 信息 | |
release_url = f"https://api.github.com/repos/{repository}/releases/tags/{tag_name}" | |
headers = {'Authorization': f'token {github_token}'} | |
response = requests.get(release_url, headers=headers) | |
if response.status_code != 200: | |
print(f"❌ Failed to get release info: {response.status_code}") | |
sys.exit(1) | |
release_data = response.json() | |
assets = release_data.get('assets', []) | |
print(f"📦 Found {len(assets)} assets to upload") | |
# 创建版本目录 | |
version_path = f"releases/{tag_name.replace('v', '')}" | |
uploaded_files = [] | |
# 上传每个资源文件 | |
for asset in assets: | |
asset_name = asset['name'] | |
download_url = asset['browser_download_url'] | |
print(f"📥 Downloading: {asset_name}") | |
# 下载文件 | |
asset_response = requests.get(download_url, headers=headers) | |
if asset_response.status_code != 200: | |
print(f"❌ Failed to download {asset_name}") | |
continue | |
# 上传到 COS | |
cos_key = f"{version_path}/{asset_name}" | |
try: | |
print(f"☁️ Uploading to COS: {cos_key}") | |
client.put_object( | |
Bucket=bucket, | |
Body=asset_response.content, | |
Key=cos_key, | |
ContentType='application/octet-stream' | |
) | |
# 构建 CDN URL | |
cdn_file_url = f"{cdn_url.rstrip('/')}/{cos_key}" | |
uploaded_files.append({ | |
'name': asset_name, | |
'cos_key': cos_key, | |
'cdn_url': cdn_file_url, | |
'size': len(asset_response.content) | |
}) | |
print(f"✅ Successfully uploaded: {asset_name}") | |
print(f" CDN URL: {cdn_file_url}") | |
except Exception as e: | |
print(f"❌ Failed to upload {asset_name}: {str(e)}") | |
# 创建索引文件 | |
index_data = { | |
'version': tag_name, | |
'upload_time': int(time.time()), | |
'files': uploaded_files | |
} | |
index_key = f"{version_path}/index.json" | |
try: | |
client.put_object( | |
Bucket=bucket, | |
Body=json.dumps(index_data, indent=2).encode('utf-8'), | |
Key=index_key, | |
ContentType='application/json' | |
) | |
print(f"📋 Created index file: {cdn_url.rstrip('/')}/{index_key}") | |
except Exception as e: | |
print(f"❌ Failed to create index file: {str(e)}") | |
print(f"🎉 COS upload completed! Uploaded {len(uploaded_files)} files") | |
print(f"📁 Version directory: {cdn_url.rstrip('/')}/{version_path}/") | |
if __name__ == "__main__": | |
main() | |
EOF | |
python upload_to_cos.py | |
env: | |
GITHUB_TOKEN: ${{ secrets.atriordsa }} | |
COS_SECRET_ID: ${{ secrets.COS_SECRET_ID }} | |
COS_SECRET_KEY: ${{ secrets.COS_SECRET_KEY }} | |
BUCKET: ${{ secrets.BUCKET }} | |
REGION: ${{ secrets.REGION }} | |
CDN_URL: ${{ secrets.CDN_URL }} | |
TAG_NAME: ${{ github.ref_name }} | |
REPOSITORY: ${{ github.repository }} |