diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 371fc81..757ba94 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,11 +6,6 @@ on: - v* workflow_dispatch: inputs: - test_upload_dist: - description: "Test upload-dist.js script" - required: true - default: false - type: boolean build_os: description: "Build for specific OS: Windows, macOS, Linux, All" required: true @@ -21,97 +16,243 @@ on: - macOS - Linux - All + test_upload_dist: + description: "Test upload-dist.js script" + required: true + default: false + type: boolean + test_upload_dist_to_dev: + description: "Test upload-dist.js script to dev folder" + required: true + default: false + type: boolean skip_notarize: description: "Skip Notarization (true/false)" required: true default: false type: boolean + env: NODE_VERSION: 22.x jobs: - # parallel build jobs - build: - name: Build on ${{ matrix.os }} - runs-on: ${{ matrix.os }} + # ============== macOS Builds ============== + build-macos: + name: Build macOS (${{ matrix.arch }}) + if: github.event.inputs.build_os == 'macOS' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v') + runs-on: ${{ matrix.runner }} strategy: - # if one job fails, do not stop other jobs fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-latest] + include: + - runner: macos-15-intel + target: dmg + arch: x64 + - runner: macos-latest + target: dmg + arch: arm64 steps: - name: Check out git repository uses: actions/checkout@v4 + - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 + - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm cache-dependency-path: pnpm-lock.yaml - - name: Clean workspace on Windows - if: runner.os == 'Windows' && (github.event.inputs.build_os == 'Windows' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')) + + - name: Clean workspace + run: rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build macOS App (${{ matrix.arch }}) + run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --mac ${{ matrix.target }} --${{ matrix.arch }} --publish never + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + CSC_LINK: ${{ secrets.MAC_CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + SKIP_NOTARIZE: ${{ inputs.skip_notarize }} + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: PicGo-macOS-${{ matrix.arch }} + path: dist/*.* + + # ============== Windows Builds ============== + build-windows: + name: Build Windows (${{ matrix.arch }}) + if: github.event.inputs.build_os == 'Windows' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v') + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: windows-latest + arch: x64-ia32 + build_arch: --x64 --ia32 + target: nsis + - runner: windows-11-arm + arch: arm64 + build_arch: --arm64 + target: nsis + steps: + - name: Check out git repository + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - name: Clean workspace run: | if (Test-Path dist) { Remove-Item -Recurse -Force dist } if (Test-Path dist_electron) { Remove-Item -Recurse -Force dist_electron } if (Test-Path node_modules) { Remove-Item -Recurse -Force node_modules } - if (Test-Path "$env:LOCALAPPDATA\electron-builder") { Remove-Item "$env:LOCALAPPDATA\electron-builder" -Recurse -Force -ErrorAction SilentlyContinue } if (Test-Path "$env:LOCALAPPDATA\electron") { Remove-Item "$env:LOCALAPPDATA\electron" -Recurse -Force -ErrorAction SilentlyContinue } - - name: Clean workspace on macOS & Linux - if: runner.os == 'macOS' || runner.os == 'Linux' && (github.event.inputs.build_os == 'macOS' || github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')) - run: | - rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron + - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Ubuntu Update with sudo - if: runner.os == 'Linux' && (github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')) - run: | - sudo apt-get update - sudo apt-get install -y libfuse2 - - name: Build Windows x64 & ARM64 App - if: runner.os == 'Windows' && (github.event.inputs.build_os == 'Windows' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')) - run: pnpm run build:win || true - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - - name: Build macOS x64 & ARM64 App - if: runner.os == 'macOS' && (github.event.inputs.build_os == 'macOS' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')) - run: pnpm run build:mac || true - shell: bash - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - # macOS Code Signing - # p12 证书的 Base64 字符串 - CSC_LINK: ${{ secrets.MAC_CSC_LINK }} - # p12 证书密码 - CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }} - # macOS Notarization (公证所需变量) - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - SKIP_NOTARIZE: ${{ inputs.skip_notarize }} - - name: Build Linux x64 & ARM64 App - if: runner.os == 'Linux' && (github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')) - run: pnpm run build:linux || true - shell: bash + + - name: Build Windows App (${{ matrix.arch }}) + run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --win ${{matrix.target}} ${{ matrix.build_arch }} --publish never env: GH_TOKEN: ${{ secrets.GH_TOKEN }} + - name: Upload Artifact uses: actions/upload-artifact@v4 with: - name: PicGo-${{ runner.os }} + name: PicGo-Windows-${{ matrix.arch }} path: dist/*.* - - name: Upload to release.picgo.app - if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.test_upload_dist + + # ============== Linux Builds ============== + build-linux: + name: Build Linux (${{ matrix.arch }}) + if: github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v') + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + arch: x64 + target: AppImage deb snap + - runner: ubuntu-24.04-arm + arch: arm64 + target: AppImage deb + steps: + - name: Check out git repository + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - name: Clean workspace + run: rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Linux dependencies run: | - pnpm run upload-dist + sudo apt-get update + sudo apt-get install -y libfuse2 + + - name: Build Linux App (${{ matrix.arch }}) + run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --linux ${{matrix.target}} --${{ matrix.arch }} --publish never + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: PicGo-Linux-${{ matrix.arch }} + path: dist/*.* + + # ============== Release ============== + release: + name: Merge & Release + needs: [build-macos, build-windows, build-linux] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Check out git repository + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: List artifacts + run: ls -laR artifacts/ + + - name: Merge artifacts and yml files + run: node scripts/merge-artifacts.js + + - name: List dist + run: ls -la dist/ + + - name: Upload to release.picgo.app + if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.test_upload_dist || github.event.inputs.test_upload_dist_to_dev + run: | + ARGS="--all" + + if [[ "${{ github.event.inputs.test_upload_dist_to_dev }}" == "true" ]]; then + ARGS="$ARGS --dev" + echo "🚧 Test Upload Mode: ON" + fi + + node scripts/upload-dist.js $ARGS env: PICGO_ENV_S3_SECRET_ID: ${{ secrets.PICGO_ENV_S3_SECRET_ID }} PICGO_ENV_S3_SECRET_KEY: ${{ secrets.PICGO_ENV_S3_SECRET_KEY }} @@ -119,17 +260,7 @@ jobs: PICGO_ENV_S3_LEGACY_ACCOUNT_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_ACCOUNT_ID }} PICGO_ENV_S3_LEGACY_SECRET_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_ID }} PICGO_ENV_S3_LEGACY_SECRET_KEY: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_KEY }} - release: - name: Publish GitHub Release - needs: build - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts + - name: Publish GitHub Dev Release if: github.event_name == 'workflow_dispatch' uses: softprops/action-gh-release@v2 @@ -140,16 +271,16 @@ jobs: draft: true prerelease: false files: | - !artifacts/**/*-unpacked/** - artifacts/**/*.exe - artifacts/**/*.dmg - artifacts/**/*.zip - artifacts/**/*.AppImage - artifacts/**/*.deb - artifacts/**/*.snap - artifacts/**/*.tar.gz - artifacts/**/*.yml - artifacts/**/*.blockmap + dist/*.exe + dist/*.dmg + dist/*.zip + dist/*.AppImage + dist/*.deb + dist/*.snap + dist/*.tar.gz + dist/*.yml + dist/*.blockmap + - name: Publish GitHub Release if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v2 @@ -160,13 +291,12 @@ jobs: draft: true prerelease: false files: | - !artifacts/**/*-unpacked/** - artifacts/**/*.exe - artifacts/**/*.dmg - artifacts/**/*.zip - artifacts/**/*.AppImage - artifacts/**/*.deb - artifacts/**/*.snap - artifacts/**/*.tar.gz - artifacts/**/*.yml - artifacts/**/*.blockmap + dist/*.exe + dist/*.dmg + dist/*.zip + dist/*.AppImage + dist/*.deb + dist/*.snap + dist/*.tar.gz + dist/*.yml + dist/*.blockmap diff --git a/electron-builder.config.ts b/electron-builder.config.ts index 4965e8a..7882cd5 100644 --- a/electron-builder.config.ts +++ b/electron-builder.config.ts @@ -3,7 +3,7 @@ import dotenv from 'dotenv' dotenv.config() -const shouldNotarize = process.env.SKIP_NOTARIZE !== 'true'; +const shouldNotarize = process.env.SKIP_NOTARIZE !== 'true' const config: Configuration = { appId: 'com.molunerfinn.picgo', diff --git a/package.json b/package.json index 40b0d79..57b224c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "email": "marksz@teamsz.xyz" }, "scripts": { - "build": "electron-vite build && electron-builder --config electron-builder.config.ts --publish never", + "build": "electron-vite build", "build:win": "npm run build && electron-builder --config electron-builder.config.ts --win --publish never", "build:mac": "npm run build && electron-builder --config electron-builder.config.ts --mac --publish never", "build:linux": "npm run build && electron-builder --config electron-builder.config.ts --linux --publish never", @@ -44,7 +44,7 @@ "element-plus": "^2.3.7", "epipebomb": "^1.0.0", "fs-extra": "^10.0.0", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "keycode": "^2.2.0", "lodash": "^4.17.21", "lodash-id": "^0.14.0", @@ -132,4 +132,4 @@ "commit-msg": "npm run lint:dpdm && commitlint -E HUSKY_GIT_PARAMS" } } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfe5f94..4307c2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,7 +45,7 @@ importers: specifier: ^10.0.0 version: 10.1.0 js-yaml: - specifier: ^4.1.0 + specifier: ^4.1.1 version: 4.1.1 keycode: specifier: ^2.2.0 diff --git a/scripts/cos-link.js b/scripts/cos-link.js index f65c825..bfe2366 100644 --- a/scripts/cos-link.js +++ b/scripts/cos-link.js @@ -16,15 +16,15 @@ const macos = [ const linux = { AppImage: [ - { label: '64 bit', arch: '-x64', ext: '.AppImage' }, + { label: '64 bit', arch: '-amd64', ext: '.AppImage' }, { label: 'ARM64', arch: '-arm64', ext: '.AppImage' } ], Deb: [ - { label: '64 bit', arch: '-x64', ext: '.deb' }, + { label: '64 bit', arch: '-amd64', ext: '.deb' }, { label: 'ARM64', arch: '-arm64', ext: '.deb' } ], Snap: [ - { label: '64 bit', arch: '-x64', ext: '.snap' } + { label: '64 bit', arch: '-amd64', ext: '.snap' } ] } diff --git a/scripts/merge-artifacts.js b/scripts/merge-artifacts.js new file mode 100644 index 0000000..2cf06b7 --- /dev/null +++ b/scripts/merge-artifacts.js @@ -0,0 +1,197 @@ +/** + * Merge artifacts from different platforms and architectures + * Also merge latest*.yml files for electron-updater + */ + +const fs = require('fs') +const path = require('path') +const yaml = require('js-yaml') + +const ARTIFACTS_DIR = path.join(__dirname, '../artifacts') +const DIST_DIR = path.join(__dirname, '../dist') + +// yml 文件分组规则 +const YML_MERGE_RULES = { + // macOS: 合并 x64 和 arm64 的 latest-mac.yml + 'latest-mac.yml': ['latest-mac.yml'], + // Windows: 合并所有架构的 latest.yml + 'latest.yml': ['latest.yml'], + // Linux x64: latest-linux.yml + 'latest-linux.yml': ['latest-linux.yml'], + // Linux arm64: latest-linux-arm64.yml + 'latest-linux-arm64.yml': ['latest-linux-arm64.yml'] +} + +/** + * 递归查找指定文件名的所有文件 + */ +function findFiles(dir, filename) { + const results = [] + + if (!fs.existsSync(dir)) { + return results + } + + const items = fs.readdirSync(dir) + + for (const item of items) { + const fullPath = path.join(dir, item) + const stat = fs.statSync(fullPath) + + if (stat.isDirectory()) { + results.push(...findFiles(fullPath, filename)) + } else if (item === filename) { + results.push(fullPath) + } + } + + return results +} + +/** + * 合并多个 yml 文件 + */ +function mergeYmlFiles(files) { + if (files.length === 0) return null + if (files.length === 1) { + return yaml.load(fs.readFileSync(files[0], 'utf8')) + } + + const contents = files.map(f => yaml.load(fs.readFileSync(f, 'utf8'))) + + // 以第一个为基准,合并 files 数组 + const merged = { + version: contents[0].version, + files: [], + releaseDate: contents[0].releaseDate + } + + for (const content of contents) { + if (content.files && Array.isArray(content.files)) { + merged.files.push(...content.files) + } + } + + // 去重(根据 sha512) + const seen = new Set() + merged.files = merged.files.filter(file => { + const key = file.sha512 + if (seen.has(key)) return false + seen.add(key) + return true + }) + + // 设置 path/sha512/size 为第一个文件(electron-updater 兼容性) + if (merged.files.length > 0) { + merged.path = merged.files[0].url + merged.sha512 = merged.files[0].sha512 + merged.size = merged.files[0].size + } + + return merged +} + +/** + * 复制所有构建产物到 dist 目录 + */ +function copyArtifacts() { + console.log('📁 Copying all artifacts to dist...\n') + + if (!fs.existsSync(ARTIFACTS_DIR)) { + console.log('⚠️ No artifacts directory found') + return + } + + const platformDirs = fs.readdirSync(ARTIFACTS_DIR) + + for (const platformDir of platformDirs) { + const platformPath = path.join(ARTIFACTS_DIR, platformDir) + const stat = fs.statSync(platformPath) + + if (!stat.isDirectory()) continue + + console.log(`📦 Processing ${platformDir}...`) + const files = fs.readdirSync(platformPath) + + for (const file of files) { + const srcPath = path.join(platformPath, file) + const destPath = path.join(DIST_DIR, file) + const fileStat = fs.statSync(srcPath) + + // 跳过目录和 yml 文件(yml 文件会单独处理合并) + if (fileStat.isDirectory()) continue + if (file.endsWith('.yml')) continue + + // 如果目标文件已存在且大小相同,跳过 + if (fs.existsSync(destPath)) { + const destStat = fs.statSync(destPath) + if (destStat.size === fileStat.size) { + console.log(` ⏭️ Skipped (exists): ${file}`) + continue + } + } + + fs.copyFileSync(srcPath, destPath) + console.log(` ✅ Copied: ${file}`) + } + } +} + +/** + * 合并 yml 文件 + */ +function mergeYmlFilesFromArtifacts() { + console.log('\n🔀 Merging yml files...\n') + + for (const [outputName, sourceNames] of Object.entries(YML_MERGE_RULES)) { + const allFiles = [] + + for (const sourceName of sourceNames) { + const files = findFiles(ARTIFACTS_DIR, sourceName) + allFiles.push(...files) + } + + if (allFiles.length === 0) { + console.log(`⏭️ No ${outputName} found, skipping...`) + continue + } + + console.log(`📄 Found ${allFiles.length} ${outputName} file(s):`) + allFiles.forEach(f => console.log(` - ${path.relative(ARTIFACTS_DIR, f)}`)) + + const merged = mergeYmlFiles(allFiles) + + if (merged) { + const outputPath = path.join(DIST_DIR, outputName) + fs.writeFileSync(outputPath, yaml.dump(merged, { lineWidth: -1 })) + console.log(`✅ Merged -> ${outputName}`) + + if (merged.files) { + console.log(` Files: ${merged.files.map(f => f.url).join(', ')}`) + } + console.log('') + } + } +} + +async function main() { + console.log('🚀 Starting artifact merge process...\n') + + // 确保 dist 目录存在 + if (!fs.existsSync(DIST_DIR)) { + fs.mkdirSync(DIST_DIR, { recursive: true }) + } + + // 1. 复制所有构建产物 + copyArtifacts() + + // 2. 合并 yml 文件 + mergeYmlFilesFromArtifacts() + + console.log('🎉 Artifact merge completed!') +} + +main().catch(err => { + console.error('❌ Error:', err) + process.exit(1) +}) diff --git a/scripts/upload-dist.js b/scripts/upload-dist.js index b464bf2..85f9d01 100644 --- a/scripts/upload-dist.js +++ b/scripts/upload-dist.js @@ -2,8 +2,6 @@ // upload version file to cos require('dotenv').config() -// const crypto = require('crypto') -// const axios = require('axios').default const fs = require('fs') const pkg = require('../package.json') const configList = require('./config') @@ -12,15 +10,13 @@ const path = require('path') const distPath = path.join(__dirname, '../dist') const S3Client = require('@aws-sdk/client-s3').S3Client const Upload = require('@aws-sdk/lib-storage').Upload -// const BUCKET = 'picgo-1251750343' -// const COS_SECRET_ID = process.env.PICGO_ENV_COS_SECRET_ID -// const COS_SECRET_KEY = process.env.PICGO_ENV_COS_SECRET_KEY +const uploadToDev = process.argv.includes('--dev') const S3_BUCKET = 'release' const S3_LEGACY_BUCKET = 'picgo' -// const AREA = 'ap-chengdu' const VERSION = pkg.version -const FILE_PATH = `${VERSION}/` +const DEV_DIST_PREFIX = 'dev/' +const FILE_PATH = uploadToDev ? `${DEV_DIST_PREFIX}${VERSION}/` : `${VERSION}/` const S3_SECRET_ID = process.env.PICGO_ENV_S3_SECRET_ID const S3_SECRET_KEY = process.env.PICGO_ENV_S3_SECRET_KEY const S3_ACCOUNT_ID = process.env.PICGO_ENV_S3_ACCOUNT_ID @@ -49,155 +45,130 @@ const S3LegacyOptions = { region: 'auto' } -// https://cloud.tencent.com/document/product/436/7778#signature -// /** -// * @param {string} fileName -// * @returns -// */ -// const generateSignature = (fileName, folder = FILE_PATH) => { -// const secretKey = COS_SECRET_ID -// // const area = AREA -// const bucket = BUCKET -// const path = folder -// const today = Math.floor(new Date().getTime() / 1000) -// const tomorrow = today + 86400 -// const signTime = `${today};${tomorrow}` -// const signKey = crypto.createHmac('sha1', secretKey).update(signTime).digest('hex') -// const httpString = `put\n/${path}${fileName}\n\nhost=${bucket}.cos.accelerate.myqcloud.com\n` -// const sha1edHttpString = crypto.createHash('sha1').update(httpString).digest('hex') -// const stringToSign = `sha1\n${signTime}\n${sha1edHttpString}\n` -// const signature = crypto.createHmac('sha1', signKey).update(stringToSign).digest('hex') -// return { -// signature, -// signTime -// } -// } - -// /** -// * -// * @param {string} fileName -// * @param {Buffer} fileBuffer -// * @param {{ signature: string, signTime: string }} signature -// * @returns -// */ -// const getReqOptions = (fileName, fileBuffer, signature, folder = FILE_PATH) => { -// return { -// method: 'PUT', -// url: `http://${BUCKET}.cos.accelerate.myqcloud.com/${encodeURI(folder)}${encodeURI(fileName)}`, -// headers: { -// Host: `${BUCKET}.cos.accelerate.myqcloud.com`, -// Authorization: `q-sign-algorithm=sha1&q-ak=${COS_SECRET_KEY}&q-sign-time=${signature.signTime}&q-key-time=${signature.signTime}&q-header-list=host&q-url-param-list=&q-signature=${signature.signature}`, -// contentType: mime.lookup(fileName), -// useAgent: `PicGo;${pkg.version};null;null` -// }, -// maxContentLength: Infinity, -// maxBodyLength: Infinity, -// data: fileBuffer, -// resolveWithFullResponse: true -// } -// } +/** + * 检查是否使用 --all 参数(上传所有平台) + */ +function shouldUploadAll() { + return process.argv.includes('--all') +} /** - * a backup for version file + * 获取要上传的配置列表 */ -// const uploadVersionFile = async () => { -// try { -// const platform = process.platform -// if (configList[platform]) { -// let versionFileHasUploaded = false -// for (const [, config] of configList[platform].entries()) { -// const versionFilePath = path.join(distPath, config['version-file']) -// let versionFileName = config['version-file'] -// if (VERSION.toLocaleLowerCase().includes('beta')) { -// versionFileName = versionFileName.replace('.yml', '.beta.yml') -// } -// // upload version file -// if (!versionFileHasUploaded) { -// const signature = generateSignature(versionFileName, '') -// const reqOptions = getReqOptions(versionFileName, fs.readFileSync(versionFilePath), signature, '') -// console.log('[PicGo Version File] Uploading...', versionFileName) -// await axios.request(reqOptions) -// versionFileHasUploaded = true -// } -// } -// } else { -// console.warn('platform not supported!', platform) -// } -// } catch (e) { -// console.error(e) -// } -// } +function getUploadConfigs() { + if (shouldUploadAll()) { + // 合并所有平台的配置 + return [ + ...configList.darwin, + ...configList.win32, + ...configList.linux + ] + } + // 原有逻辑:根据当前平台 + const platform = process.platform + return configList[platform] || [] +} + +/** + * 上传单个文件到 S3 + */ +async function uploadFileToS3(client, bucket, key, filePath, contentType = 'application/octet-stream') { + const upload = new Upload({ + client, + params: { + Bucket: bucket, + Key: key, + Body: fs.createReadStream(filePath), + ContentType: contentType + } + }) + + upload.on('httpUploadProgress', progress => { + const percent = progress.total ? Math.round((progress.loaded / progress.total) * 100) : 0 + process.stdout.write(`\r Progress: ${progress.loaded}/${progress.total || '?'} (${percent}%)`) + }) + + await upload.done() + console.log('') // 换行 +} const uploadDist = async () => { try { - const platform = process.platform - if (configList[platform]) { - const uploadedVersionFiles = new Set() - for (const [index, config] of configList[platform].entries()) { - const fileName = `${config.appNameWithPrefix}-${VERSION}-${config.arch}.${config.ext}` - const filePath = path.join(distPath, fileName) - const versionFilePath = path.join(distPath, config['version-file']) - let versionFileName = config['version-file'] - if (VERSION.toLocaleLowerCase().includes('beta')) { - versionFileName = versionFileName.replace('.yml', '.beta.yml') - } - console.log('[PicGo Dist] Preparing to upload', fileName) - const client = new S3Client(S3Options) - if (fs.existsSync(filePath)) { - const uploadDistToS3 = new Upload({ - client, - params: { - Bucket: S3_BUCKET, - Key: `${FILE_PATH}${fileName}`, - Body: fs.createReadStream(filePath), - ContentType: 'application/octet-stream' - } - }) - // upload dist file - console.log('[PicGo Dist] Uploading...', fileName, `${index + 1}/${configList[platform].length}`) - uploadDistToS3.on('httpUploadProgress', progress => { - console.log(`[PicGo Dist] Uploading... ${progress.loaded}/${progress.total}`) - }) - await uploadDistToS3.done() - } else { - console.warn('[PicGo Dist] File not found:', fileName) - } + const configs = getUploadConfigs() - // upload version file - if (!uploadedVersionFiles.has(versionFileName) && fs.existsSync(versionFilePath)) { - const uploadVersionFileToS3 = new Upload({ - client, - params: { - Bucket: S3_BUCKET, - Key: `${versionFileName}`, - Body: fs.createReadStream(versionFilePath), - ContentType: mime.lookup(versionFileName) - } - }) - // upload to legacy bucket as well - // will be deprecated in 2.5.0 - const legacyClient = new S3Client(S3LegacyOptions) - const uploadVersionFileToLegacyS3 = new Upload({ - client: legacyClient, - params: { - Bucket: S3_LEGACY_BUCKET, - Key: `${versionFileName}`, - Body: fs.createReadStream(versionFilePath), - ContentType: mime.lookup(versionFileName) - } - }) - console.log('[PicGo Version File] Uploading...', versionFileName) - await uploadVersionFileToS3.done() - await uploadVersionFileToLegacyS3.done() - uploadedVersionFiles.add(versionFileName) - console.log('[PicGo Version File] Upload successfully') - } - } - } else { - console.warn('platform not supported!', platform) + if (configs.length === 0) { + console.warn('[PicGo] No upload config found!') + return } + + console.log(`[PicGo] Upload mode: ${shouldUploadAll() ? 'ALL PLATFORMS' : process.platform}`) + console.log(`[PicGo] Version: ${VERSION}`) + console.log(`[PicGo] Total files to upload: ${configs.length}\n`) + + const uploadedVersionFiles = new Set() + const client = new S3Client(S3Options) + const legacyClient = new S3Client(S3LegacyOptions) + + for (const [index, config] of configs.entries()) { + const fileName = `${config.appNameWithPrefix}-${VERSION}-${config.arch}.${config.ext}` + const filePath = path.join(distPath, fileName) + let versionFileName = config['version-file'] + + console.log(`[${index + 1}/${configs.length}] Processing ${fileName}`) + + // 上传构建产物 + if (fs.existsSync(filePath)) { + console.log(` Uploading to S3: ${FILE_PATH}${fileName}`) + await uploadFileToS3(client, S3_BUCKET, `${FILE_PATH}${fileName}`, filePath) + console.log(` ✅ Uploaded: ${fileName}`) + } else { + console.warn(` ⚠️ File not found: ${fileName}`) + } + + let versionFilePath = path.join(distPath, versionFileName) + // Beta 版本使用不同的 yml 文件名 + if (VERSION.toLowerCase().includes('beta') && fs.existsSync(versionFilePath)) { + versionFileName = versionFileName.replace('.yml', '.beta.yml') + const betaVersionFilePath = path.join(distPath, versionFileName) + // change to beta version file path + fs.renameSync(versionFilePath, betaVersionFilePath) + versionFilePath = betaVersionFilePath + } + + // 上传版本文件(每个 yml 只上传一次) + if (!uploadedVersionFiles.has(versionFileName) && fs.existsSync(versionFilePath)) { + console.log(` Uploading version file: ${versionFileName}`) + const versionFileNameFinal = uploadToDev ? `${DEV_DIST_PREFIX}${versionFileName}` : versionFileName + + // 上传到主 bucket + await uploadFileToS3( + client, + S3_BUCKET, + versionFileNameFinal, + versionFilePath, + mime.lookup(versionFileName) || 'text/yaml' + ) + + // 上传到 legacy bucket + await uploadFileToS3( + legacyClient, + S3_LEGACY_BUCKET, + versionFileNameFinal, + versionFilePath, + mime.lookup(versionFileName) || 'text/yaml' + ) + + uploadedVersionFiles.add(versionFileName) + console.log(` ✅ Version file uploaded: ${versionFileName}`) + } + + console.log('') + } + + console.log('[PicGo] 🎉 All uploads completed!') } catch (e) { - console.error(e) + console.error('[PicGo] ❌ Upload error:', e) + process.exit(1) } }