diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4497b79..edddd2f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -154,14 +154,6 @@ jobs: --arch "${{ matrix.arch }}" --download true shell: bash - - name: Prepare Windows legacy updater payload - if: matrix.os == 'windows' - shell: pwsh - run: | - $updateDir = "release/update-${{ matrix.arch }}" - New-Item -ItemType Directory -Force -Path $updateDir | Out-Null - Copy-Item -LiteralPath "release/${{ steps.package.outputs.stem }}/EasyCLIProxyAPI.exe" -Destination $updateDir - Copy-Item -LiteralPath "release/${{ steps.package.outputs.stem }}/portable-app.json" -Destination $updateDir - name: Build signed macOS DMG if: matrix.os == 'darwin' shell: bash @@ -267,21 +259,12 @@ jobs: shell: pwsh run: | Compress-Archive -Path "release/${{ steps.package.outputs.stem }}" -DestinationPath "${{ steps.package.outputs.stem }}.zip" - $updateArchive = "EasyCLIProxyAPI-update-v${{ steps.package.outputs.version }}-Windows-${{ matrix.arch }}.zip" - Compress-Archive -Path "release/update-${{ matrix.arch }}/*" -DestinationPath $updateArchive - uses: actions/upload-artifact@v7 with: name: ${{ steps.package.outputs.stem }} path: ${{ steps.package.outputs.stem }}.${{ matrix.suffix }} if-no-files-found: error retention-days: 14 - - uses: actions/upload-artifact@v7 - if: matrix.os == 'windows' - with: - name: portable-updater-windows-${{ matrix.arch }} - path: EasyCLIProxyAPI-update-v${{ steps.package.outputs.version }}-Windows-${{ matrix.arch }}.zip - if-no-files-found: error - retention-days: 14 publish: name: Publish GitHub release if: >- diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs index 69e6a20..3654331 100644 --- a/scripts/manifest.mjs +++ b/scripts/manifest.mjs @@ -34,9 +34,9 @@ export async function generatePortableUpdateManifest({ }) { const resolvedDirectory = resolve(directory ?? 'artifacts'); const platformSpecs = { - windows: { display: 'Windows', suffix: 'zip', legacy: true }, - linux: { display: 'Linux', suffix: 'tar.gz', legacy: false }, - darwin: { display: 'Darwin', suffix: 'dmg', legacy: false }, + windows: { display: 'Windows', suffix: 'zip' }, + linux: { display: 'Linux', suffix: 'tar.gz' }, + darwin: { display: 'Darwin', suffix: 'dmg' }, }; const normalizedPlatform = String(platform).trim().toLowerCase(); const platformSpec = platformSpecs[normalizedPlatform]; @@ -64,35 +64,26 @@ export async function generatePortableUpdateManifest({ } const assets = {}; - const fullAssets = {}; for (const arch of ['amd64', 'aarch64']) { - const fullFilename = `EasyCLIProxyAPI-${tag}-${platformSpec.display}-${arch}.${platformSpec.suffix}`; - const candidates = platformSpec.legacy - ? [ - [assets, `EasyCLIProxyAPI-update-${tag}-Windows-${arch}.zip`], - [fullAssets, fullFilename], - ] - : [[assets, fullFilename]]; - for (const [collection, filename] of candidates) { - const path = join(resolvedDirectory, filename); - const [contents, metadata] = await Promise.all([readFile(path), stat(path)]); - if (!metadata.isFile() || metadata.size === 0) { - throw new Error(`Portable release asset is empty or not a file: ${filename}`); - } - const asset = { - url: `https://github.com/${resolvedRepository}/releases/download/${tag}/${filename}`, - sha256: createHash('sha256').update(contents).digest('hex'), - sizeBytes: metadata.size, - }; - if (resolvedGitcodeRepository) { - asset.fallbackUrls = [gitcodeReleaseAttachmentUrl( - resolvedGitcodeRepository, - tag, - filename, - )]; - } - collection[`${normalizedPlatform}-${arch}`] = asset; + const filename = `EasyCLIProxyAPI-${tag}-${platformSpec.display}-${arch}.${platformSpec.suffix}`; + const path = join(resolvedDirectory, filename); + const [contents, metadata] = await Promise.all([readFile(path), stat(path)]); + if (!metadata.isFile() || metadata.size === 0) { + throw new Error(`Portable release asset is empty or not a file: ${filename}`); } + const asset = { + url: `https://github.com/${resolvedRepository}/releases/download/${tag}/${filename}`, + sha256: createHash('sha256').update(contents).digest('hex'), + sizeBytes: metadata.size, + }; + if (resolvedGitcodeRepository) { + asset.fallbackUrls = [gitcodeReleaseAttachmentUrl( + resolvedGitcodeRepository, + tag, + filename, + )]; + } + assets[`${normalizedPlatform}-${arch}`] = asset; } const manifest = { @@ -101,7 +92,6 @@ export async function generatePortableUpdateManifest({ publishedAt, releaseUrl: `https://github.com/${resolvedRepository}/releases/tag/${tag}`, assets, - ...(platformSpec.legacy ? { fullAssets } : {}), }; await writeFile(resolvedOutput, `${JSON.stringify(manifest, null, 2)}\n`); @@ -125,6 +115,7 @@ async function main() { const outputs = requestedOutput ? [resolve(requestedOutput)] : portableUpdateManifestNames(platform).map((name) => join(directory, name)); + const publishedAt = new Date().toISOString(); let manifest; for (const output of outputs) { manifest = await generatePortableUpdateManifest({ @@ -134,9 +125,12 @@ async function main() { repository: args.get('--repository'), gitcodeRepository: args.get('--gitcode-repository'), tag: args.get('--tag'), + publishedAt, }); } - console.log(`Generated ${outputs.map(basename).join(', ')} for v${manifest.version}`); + console.log( + `Generated ${outputs.map((output) => basename(output)).join(', ')} for v${manifest.version}`, + ); } const entryPoint = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ''; diff --git a/tests/portableUpdateManifest.test.ts b/tests/portableUpdateManifest.test.ts index 17ef07e..2ea32d2 100644 --- a/tests/portableUpdateManifest.test.ts +++ b/tests/portableUpdateManifest.test.ts @@ -1,7 +1,9 @@ import { createHash } from 'node:crypto'; +import { spawnSync } from 'node:child_process'; import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { describe, expect, test } from 'bun:test'; import { generatePortableUpdateManifest, @@ -20,6 +22,42 @@ test('macOS uses a new primary channel and keeps the legacy manifest alias', () expect(portableUpdateManifestNames('linux')).toEqual(['portable-update-linux.json']); }); +test('macOS manifest CLI publishes both update channels', async () => { + const root = await mkdtemp(join(tmpdir(), 'easycli-darwin-manifest-cli-')); + try { + for (const arch of ['amd64', 'aarch64']) { + await writeFile( + join(root, `EasyCLIProxyAPI-v1.2.3-Darwin-${arch}.dmg`), + `darwin ${arch} release`, + ); + } + const script = fileURLToPath(new URL('../scripts/manifest.mjs', import.meta.url)); + const result = spawnSync('node', [ + script, + '--directory', + root, + '--platform', + 'darwin', + '--repository', + 'router-for-me/EasyCLIProxyAPI', + '--tag', + 'v1.2.3', + ], { encoding: 'utf8' }); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(''); + const primary = JSON.parse( + await readFile(join(root, 'portable-update-darwin-v2.json'), 'utf8'), + ); + const legacy = JSON.parse( + await readFile(join(root, 'portable-update-darwin.json'), 'utf8'), + ); + expect(legacy).toEqual(primary); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + describe('Windows 便携更新清单', () => { test('URL、大小和哈希与实际上传资产一致', async () => { const root = await mkdtemp(join(tmpdir(), 'easycli-manifest-test-')); @@ -29,10 +67,6 @@ describe('Windows 便携更新清单', () => { aarch64: Buffer.from('aarch64 full portable package'), }; for (const [arch, contents] of Object.entries(payloads)) { - await writeFile( - join(root, `EasyCLIProxyAPI-update-v1.2.3-Windows-${arch}.zip`), - Buffer.from(`${arch} legacy update package`), - ); await writeFile( join(root, `EasyCLIProxyAPI-v1.2.3-Windows-${arch}.zip`), contents, @@ -54,21 +88,15 @@ describe('Windows 便携更新清单', () => { for (const arch of ['amd64', 'aarch64'] as const) { const asset = manifest.assets[`windows-${arch}`]; expect(asset.url).toBe( - `https://github.com/router-for-me/EasyCLIProxyAPI/releases/download/v1.2.3/EasyCLIProxyAPI-update-v1.2.3-Windows-${arch}.zip`, - ); - expect(asset.fallbackUrls).toEqual([ - `https://api.gitcode.com/api/v5/repos/mirror-owner/EasyCLIProxyAPI/releases/v1.2.3/attach_files/EasyCLIProxyAPI-update-v1.2.3-Windows-${arch}.zip/download`, - ]); - const fullAsset = manifest.fullAssets[`windows-${arch}`]; - expect(fullAsset.url).toBe( `https://github.com/router-for-me/EasyCLIProxyAPI/releases/download/v1.2.3/EasyCLIProxyAPI-v1.2.3-Windows-${arch}.zip`, ); - expect(fullAsset.fallbackUrls).toEqual([ + expect(asset.fallbackUrls).toEqual([ `https://api.gitcode.com/api/v5/repos/mirror-owner/EasyCLIProxyAPI/releases/v1.2.3/attach_files/EasyCLIProxyAPI-v1.2.3-Windows-${arch}.zip/download`, ]); - expect(fullAsset.sizeBytes).toBe(payloads[arch].byteLength); - expect(fullAsset.sha256).toBe(createHash('sha256').update(payloads[arch]).digest('hex')); + expect(asset.sizeBytes).toBe(payloads[arch].byteLength); + expect(asset.sha256).toBe(createHash('sha256').update(payloads[arch]).digest('hex')); } + expect(manifest.fullAssets).toBeUndefined(); } finally { await rm(root, { recursive: true, force: true }); } @@ -78,7 +106,7 @@ describe('Windows 便携更新清单', () => { const root = await mkdtemp(join(tmpdir(), 'easycli-manifest-missing-')); try { await writeFile( - join(root, 'EasyCLIProxyAPI-update-v1.2.3-Windows-amd64.zip'), + join(root, 'EasyCLIProxyAPI-v1.2.3-Windows-amd64.zip'), 'amd64', ); await expect(generatePortableUpdateManifest({ @@ -92,43 +120,6 @@ describe('Windows 便携更新清单', () => { } }); - test('所有版本同时保留旧版更新包和完整包', async () => { - const root = await mkdtemp(join(tmpdir(), 'easycli-manifest-dual-')); - try { - for (const arch of ['amd64', 'aarch64']) { - await writeFile( - join(root, `EasyCLIProxyAPI-update-v1.2.3-Windows-${arch}.zip`), - `legacy update ${arch}`, - ); - await writeFile( - join(root, `EasyCLIProxyAPI-v1.2.3-Windows-${arch}.zip`), - `full package ${arch}`, - ); - } - const manifest = await generatePortableUpdateManifest({ - directory: root, - output: join(root, 'portable-update-windows.json'), - repository: 'router-for-me/EasyCLIProxyAPI', - tag: 'v1.2.3', - publishedAt: '2026-08-02T00:00:00.000Z', - }); - - expect(manifest.assets['windows-amd64'].url).toEndWith( - '/EasyCLIProxyAPI-update-v1.2.3-Windows-amd64.zip', - ); - expect(manifest.assets['windows-aarch64'].url).toEndWith( - '/EasyCLIProxyAPI-update-v1.2.3-Windows-aarch64.zip', - ); - expect(manifest.fullAssets['windows-amd64'].url).toEndWith( - '/EasyCLIProxyAPI-v1.2.3-Windows-amd64.zip', - ); - expect(manifest.fullAssets['windows-aarch64'].url).toEndWith( - '/EasyCLIProxyAPI-v1.2.3-Windows-aarch64.zip', - ); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); }); describe('跨平台便携更新清单', () => {