Skip to content

replace Python SDK with JavaScript SDK for COS upload #82

replace Python SDK with JavaScript SDK for COS upload

replace Python SDK with JavaScript SDK for COS upload #82

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
timeout-minutes: 180 # 3小时超时,适合大文件上传
needs: [cleanup-release]
if: always() && needs.cleanup-release.result == 'success'
steps:
- name: Get release info
id: release
run: |
set -e
echo "🔍 Getting release info for tag: ${{ github.ref_name }}"
# 验证环境变量
if [ -z "$GITHUB_TOKEN" ]; then
echo "❌ GITHUB_TOKEN not set"
exit 1
fi
# 等待确保 cleanup 完成
echo "⏰ Waiting 15 seconds for cleanup to complete..."
sleep 15
# 首先尝试通过标签获取 release(与 cleanup-release 相同的方法)
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')
TAG_NAME=$(echo "$RELEASE_RESPONSE" | jq -r '.tag_name')
ASSETS=$(echo "$RELEASE_RESPONSE" | jq -c '.assets')
echo "✅ Found release: $TAG_NAME (ID: $RELEASE_ID)"
echo "📦 Assets count: $(echo "$ASSETS" | jq length)"
# 输出到 GitHub Actions outputs
echo "release_id=$RELEASE_ID" >> $GITHUB_OUTPUT
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
echo "assets<<EOF" >> $GITHUB_OUTPUT
echo "$ASSETS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "✅ Release info prepared for COS upload"
env:
GITHUB_TOKEN: ${{ secrets.atriordsa }}
- name: Download release assets
id: download
env:
GITHUB_TOKEN: ${{ secrets.atriordsa }}
run: |
echo "📥 Starting download of release assets..."
# 验证环境变量
if [ -z "$GITHUB_TOKEN" ]; then
echo "❌ GITHUB_TOKEN not set"
exit 1
fi
# 创建下载目录
mkdir -p ./downloads
cd ./downloads
# 解析 assets JSON
echo '${{ steps.release.outputs.assets }}' > assets.json
# 检查是否有assets
ASSET_COUNT=$(jq 'length' assets.json)
echo "📦 Found $ASSET_COUNT assets to download"
if [ "$ASSET_COUNT" -eq 0 ]; then
echo "❌ No assets found to download"
exit 1
fi
# 直接从tagged release构建正确的URL
TAG_NAME="${{ steps.release.outputs.tag_name }}"
REPO="${{ github.repository }}"
# 使用 jq 处理每个 asset,但构建正确的URL
jq -r '.[] | .name' assets.json | while read -r name; do
echo "📥 Downloading: $name"
# 构建正确的下载URL(使用tag而不是untagged)
CORRECT_URL="https://github.com/${REPO}/releases/download/${TAG_NAME}/${name}"
echo " URL: $CORRECT_URL"
# 下载文件,使用认证和更详细的错误处理
if curl -L -H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/octet-stream" \
--fail --show-error --silent \
--connect-timeout 30 \
--max-time 300 \
-o "$name" "$CORRECT_URL"; then
echo "✅ Downloaded: $name ($(du -h "$name" | cut -f1))"
# 验证文件不为空
if [ ! -s "$name" ]; then
echo "❌ Downloaded file is empty: $name"
exit 1
fi
else
echo "❌ Failed to download: $name"
echo "❌ curl exit code: $?"
# 尝试无认证下载(对于公开文件)
echo "🔄 Trying without authentication..."
if curl -L --fail --show-error --silent \
--connect-timeout 30 \
--max-time 300 \
-o "$name" "$CORRECT_URL"; then
echo "✅ Downloaded without auth: $name ($(du -h "$name" | cut -f1))"
else
echo "❌ Failed even without authentication"
echo "❌ Trying alternative URL construction..."
# 最后尝试:使用GitHub API下载
ASSET_ID=$(echo '${{ steps.release.outputs.assets }}' | jq -r ".[] | select(.name == \"$name\") | .id")
if [ -n "$ASSET_ID" ] && [ "$ASSET_ID" != "null" ]; then
echo "🔄 Trying GitHub API download (Asset ID: $ASSET_ID)..."
if curl -L -H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/octet-stream" \
--fail --show-error --silent \
--connect-timeout 30 \
--max-time 300 \
-o "$name" \
"https://api.github.com/repos/${REPO}/releases/assets/${ASSET_ID}"; then
echo "✅ Downloaded via API: $name ($(du -h "$name" | cut -f1))"
else
echo "❌ API download also failed"
exit 1
fi
else
echo "❌ Could not find asset ID"
exit 1
fi
fi
fi
done
echo "📄 Downloaded files:"
ls -la
- name: Create release index
uses: actions/github-script@v7
with:
github-token: ${{ secrets.atriordsa }}
script: |
const fs = require('fs');
const path = require('path');
const tagName = '${{ steps.release.outputs.tag_name }}';
const cdnUrl = '${{ secrets.CDN_URL }}';
const versionPath = `releases/${tagName}`;
console.log('📋 Creating release index...');
// 读取下载的文件信息
const downloadDir = './downloads';
const files = [];
if (fs.existsSync(downloadDir)) {
const fileList = fs.readdirSync(downloadDir);
for (const fileName of fileList) {
if (fileName !== 'assets.json') {
const filePath = path.join(downloadDir, fileName);
const stats = fs.statSync(filePath);
// CDN URL 应该匹配实际的上传路径
const safeKey = `${versionPath}/${fileName}`.replace(/ /g, '_');
const cdnFileUrl = `${cdnUrl.replace(/\/$/, '')}/${safeKey}`;
files.push({
name: fileName,
cos_key: safeKey,
cdn_url: cdnFileUrl,
size: stats.size
});
console.log(`📄 ${fileName} -> ${cdnFileUrl}`);
}
}
}
// 创建索引数据
const indexData = {
version: tagName,
upload_time: Math.floor(Date.now() / 1000),
files: files,
total_files: files.length,
repository: context.repo.owner + '/' + context.repo.repo
};
// 保存索引文件
const indexFile = path.join(downloadDir, 'index.json');
fs.writeFileSync(indexFile, JSON.stringify(indexData, null, 2));
console.log(`✅ Created index with ${files.length} files`);
console.log(`📁 Version directory: ${cdnUrl.replace(/\/$/, '')}/${versionPath}/`);
- name: Upload all files to COS using JavaScript SDK
uses: actions/github-script@v7
with:
github-token: ${{ secrets.atriordsa }}
script: |
const fs = require('fs');
const path = require('path');
console.log('� Setting up Tencent Cloud COS upload with JavaScript SDK...');
// 安装腾讯云COS JavaScript SDK
console.log('� Installing Tencent Cloud COS JavaScript SDK...');
const { execSync } = require('child_process');
try {
execSync('npm install cos-nodejs-sdk-v5', { stdio: 'inherit' });
console.log('✅ COS JavaScript SDK installed successfully');
} catch (error) {
console.error('❌ Failed to install COS SDK:', error.message);
process.exit(1);
}
// 导入COS SDK
console.log('📦 Importing COS SDK...');
const COS = require('cos-nodejs-sdk-v5');
console.log('✅ COS SDK imported successfully');
// 从环境变量获取配置
console.log('🔍 Reading environment variables...');
const secretId = process.env.COS_SECRET_ID;
const secretKey = process.env.COS_SECRET_KEY;
const region = process.env.COS_REGION;
const bucket = process.env.COS_BUCKET;
const tagName = process.env.TAG_NAME;
if (!secretId || !secretKey || !region || !bucket || !tagName) {
console.error('❌ Missing required environment variables');
process.exit(1);
}
console.log(`🔧 Configuring COS client for region: ${region}, bucket: ${bucket}`);
// 创建COS客户端
const cos = new COS({
SecretId: secretId,
SecretKey: secretKey,
Timeout: 1800000, // 30分钟超时,适合大文件上传
});
console.log('✅ COS client created successfully');
// 工具函数:格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + sizes[i];
}
// 检查下载目录
console.log('📁 Checking downloads directory...');
const downloadsDir = './downloads';
if (!fs.existsSync(downloadsDir)) {
console.error('❌ Downloads directory not found');
process.exit(1);
}
// 扫描文件
console.log(`📁 Scanning directory: ${downloadsDir}`);
const allFiles = fs.readdirSync(downloadsDir);
const files = allFiles.filter(f => {
const filePath = path.join(downloadsDir, f);
return fs.statSync(filePath).isFile() && f !== 'assets.json';
});
console.log(`📦 Found ${files.length} files to upload`);
// 计算总大小
let totalSize = 0;
const fileSizes = {};
for (const filename of files) {
const filePath = path.join(downloadsDir, filename);
const size = fs.statSync(filePath).size;
fileSizes[filename] = size;
totalSize += size;
}
console.log(`📊 Total size to upload: ${formatFileSize(totalSize)}`);
// 上传文件函数
function uploadFile(filePath, key) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
cos.uploadFile({
Bucket: bucket,
Region: region,
Key: key,
FilePath: filePath,
onProgress: function(progressData) {
// 可选:显示上传进度
// console.log(`Progress: ${Math.round(progressData.percent * 100)}%`);
}
}, function(err, data) {
const uploadTime = (Date.now() - startTime) / 1000;
if (err) {
console.error(`❌ Upload failed: ${err.message}`);
reject(err);
} else {
const fileSize = fileSizes[path.basename(filePath)];
const speed = uploadTime > 0 ? fileSize / uploadTime : 0;
console.log(`✅ Successfully uploaded: ${path.basename(filePath)}`);
console.log(` Time: ${uploadTime.toFixed(1)}s, Speed: ${formatFileSize(speed)}/s`);
resolve(data);
}
});
});
}
// 开始上传
const uploadedFiles = [];
let uploadedSize = 0;
let success = true;
for (let i = 0; i < files.length; i++) {
const filename = files[i];
const filePath = path.join(downloadsDir, filename);
const fileSize = fileSizes[filename];
try {
const key = `atrior/oj-competition-side-client/releases/${tagName}/${filename}`;
console.log(`📤 [${i + 1}/${files.length}] Uploading: ${filename} (${formatFileSize(fileSize)})`);
console.log(` Local: ${filePath}`);
console.log(` Remote: ${key}`);
await uploadFile(filePath, key);
uploadedSize += fileSize;
const progress = (uploadedSize / totalSize * 100).toFixed(1);
console.log(` Progress: ${uploadedSize}/${totalSize} (${progress}%)`);
uploadedFiles.push(filename);
} catch (error) {
console.error(`❌ Failed to upload ${filename}: ${error.message}`);
success = false;
}
}
// 上传摘要
console.log('\n📊 Upload Summary:');
console.log(`✅ Successfully uploaded: ${uploadedFiles.length} files`);
if (uploadedFiles.length > 0) {
uploadedFiles.forEach(f => console.log(` - ${f}`));
}
if (success) {
console.log(`\n🎉 All ${files.length} files uploaded successfully to COS!`);
console.log(`📂 Remote path: atrior/oj-competition-side-client/releases/${tagName}/`);
// 上传index.json
const indexFile = path.join(downloadsDir, 'index.json');
if (fs.existsSync(indexFile)) {
try {
const indexSize = fs.statSync(indexFile).size;
const indexKey = `atrior/oj-competition-side-client/releases/${tagName}/index.json`;
console.log(`📋 Uploading index.json (${formatFileSize(indexSize)})...`);
await uploadFile(indexFile, indexKey);
console.log('✅ Successfully uploaded index.json');
} catch (error) {
console.error(`❌ Failed to upload index.json: ${error.message}`);
}
}
} else {
console.error('\n❌ Some uploads failed');
process.exit(1);
}
env:
COS_SECRET_ID: ${{ secrets.COS_SECRET_ID }}
COS_SECRET_KEY: ${{ secrets.COS_SECRET_KEY }}
COS_REGION: ${{ secrets.COS_REGION }}
COS_BUCKET: ${{ secrets.COS_BUCKET }}
TAG_NAME: ${{ steps.release.outputs.tag_name }}
- name: Display upload summary
run: |
echo "🎉 COS upload completed successfully!"
echo "📁 Version: ${{ steps.release.outputs.tag_name }}"
echo "🌐 CDN Base URL: ${{ secrets.CDN_URL }}"
echo "📂 Release Directory: ${{ secrets.CDN_URL }}/releases/${{ steps.release.outputs.tag_name }}/"
echo "📋 Index File: ${{ secrets.CDN_URL }}/releases/${{ steps.release.outputs.tag_name }}/index.json"