From a1f93630ae8a74cc9c37b45df9c499f736846d3d Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Sun, 16 Aug 2026 02:27:07 +0800 Subject: [PATCH] fix(ci): prevent signed macOS core archives from polluting cache --- .github/workflows/release.yml | 9 ++++++- scripts/portable.mjs | 51 ++++++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73495e2..d2c6047 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -121,7 +121,7 @@ jobs: uses: actions/cache@v5 with: path: cpa-core - key: core-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('core-version.txt') }} + key: core-v2-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('core-version.txt') }} - name: Apply release version run: node scripts/set-version.mjs "${{ needs.version.outputs.version }}" - name: Resolve release package name @@ -179,6 +179,8 @@ jobs: keychain_path="$RUNNER_TEMP/apple-signing.keychain-db" keychain_password="$(uuidgen)" signed_core_dir="$RUNNER_TEMP/signed-core" + unsigned_core_archive="$RUNNER_TEMP/unsigned-core-${{ matrix.arch }}.tar.gz" + core_archive="" staging_dir="$RUNNER_TEMP/easycli-dmg-staging" bundle_dir="src-tauri/target/release/bundle" output_dmg="${{ steps.package.outputs.stem }}.dmg" @@ -194,7 +196,11 @@ jobs: cleanup() { set +e + if [ -n "$core_archive" ] && [ -f "$unsigned_core_archive" ]; then + cp "$unsigned_core_archive" "$core_archive" + fi rm -f "$certificate_path" + rm -f "$unsigned_core_archive" rm -rf "$signed_core_dir" "$staging_dir" security delete-keychain "$keychain_path" } @@ -213,6 +219,7 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path" core_archive="$(find cpa-core -maxdepth 1 -type f -name 'CLIProxyAPI_*_darwin_${{ matrix.arch }}.tar.gz' -print -quit)" + cp "$core_archive" "$unsigned_core_archive" mkdir -p "$signed_core_dir" tar -xzf "$core_archive" -C "$signed_core_dir" core_binary="$(find "$signed_core_dir" -type f -name 'cli-proxy-api' -print -quit)" diff --git a/scripts/portable.mjs b/scripts/portable.mjs index 53ceb69..c1a2eaf 100644 --- a/scripts/portable.mjs +++ b/scripts/portable.mjs @@ -30,30 +30,49 @@ const assetName = `CLIProxyAPI_${version}_${targetOS}_${targetArch}.${extension} const sourceDir = join(root, 'cpa-core'); const sourceArchive = join(sourceDir, assetName); const checksumsPath = join(sourceDir, 'checksums.txt'); +const tag = `v${version}`; +const releaseBase = `https://github.com/router-for-me/CLIProxyAPI/releases/download/${tag}`; + +const downloadReleaseFile = async (name, destination) => { + const response = await fetch(`${releaseBase}/${name}`); + if (!response.ok) throw new Error(`Download ${name} failed: HTTP ${response.status}`); + await writeFile(destination, Buffer.from(await response.arrayBuffer())); +}; + +const downloadCoreRelease = () => Promise.all([ + downloadReleaseFile(assetName, sourceArchive), + downloadReleaseFile('checksums.txt', checksumsPath), +]); + +const verifyCoreArchive = async () => { + if (!existsSync(checksumsPath)) return null; + const checksums = await readFile(checksumsPath, 'utf8'); + const expected = checksums.split(/\r?\n/).map((line) => line.trim().split(/\s+/)).find((parts) => parts[1]?.replace(/^\*/, '') === assetName)?.[0]?.toLowerCase(); + if (!expected) throw new Error(`checksums.txt does not contain ${assetName}`); + const actual = createHash('sha256').update(await readFile(sourceArchive)).digest('hex'); + return { actual, expected }; +}; await mkdir(sourceDir, { recursive: true }); if (!existsSync(sourceArchive)) { if (!shouldDownload) { throw new Error(`Built-in core archive not found: ${sourceArchive}`); } - const tag = `v${version}`; - const releaseBase = `https://github.com/router-for-me/CLIProxyAPI/releases/download/${tag}`; - const [archiveResponse, checksumsResponse] = await Promise.all([ - fetch(`${releaseBase}/${assetName}`), - fetch(`${releaseBase}/checksums.txt`), - ]); - if (!archiveResponse.ok) throw new Error(`Download ${assetName} failed: HTTP ${archiveResponse.status}`); - if (!checksumsResponse.ok) throw new Error(`Download checksums.txt failed: HTTP ${checksumsResponse.status}`); - await writeFile(sourceArchive, Buffer.from(await archiveResponse.arrayBuffer())); - await writeFile(checksumsPath, await checksumsResponse.text()); + await downloadCoreRelease(); +} else if (shouldDownload && !existsSync(checksumsPath)) { + await downloadReleaseFile('checksums.txt', checksumsPath); } -if (existsSync(checksumsPath)) { - const checksums = await readFile(checksumsPath, 'utf8'); - const expected = checksums.split(/\r?\n/).map((line) => line.trim().split(/\s+/)).find((parts) => parts[1]?.replace(/^\*/, '') === assetName)?.[0]?.toLowerCase(); - if (!expected) throw new Error(`checksums.txt does not contain ${assetName}`); - const actual = createHash('sha256').update(await readFile(sourceArchive)).digest('hex'); - if (actual !== expected) throw new Error(`SHA-256 mismatch for ${assetName}`); +let verification = await verifyCoreArchive(); +if (verification && verification.actual !== verification.expected && shouldDownload) { + console.warn(`Cached ${assetName} failed SHA-256 verification; downloading a fresh copy`); + await downloadCoreRelease(); + verification = await verifyCoreArchive(); +} +if (verification && verification.actual !== verification.expected) { + throw new Error( + `SHA-256 mismatch for ${assetName}: expected ${verification.expected}, got ${verification.actual}`, + ); } await mkdir(output, { recursive: true });