From ff1ac4ca5afff61d8d67abaab03d185f3f2bae79 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Wed, 5 Aug 2026 17:11:01 +0800 Subject: [PATCH] fix(release): stream GitCode asset uploads with curl --- scripts/publish-gitcode-release.mjs | 47 ++++++++++++++++++++++++----- tests/gitcodeRelease.test.ts | 14 ++++++--- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/scripts/publish-gitcode-release.mjs b/scripts/publish-gitcode-release.mjs index 6c238b3..3cc245e 100644 --- a/scripts/publish-gitcode-release.mjs +++ b/scripts/publish-gitcode-release.mjs @@ -1,4 +1,5 @@ -import { readdir, readFile } from 'node:fs/promises'; +import { spawn } from 'node:child_process'; +import { readdir } from 'node:fs/promises'; import { basename, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -46,6 +47,39 @@ async function apiRequest(path, { return response.status === 204 || response.status === 404 ? null : response.json(); } +export function uploadFileWithCurl({ url, headers, path }) { + const args = [ + '--fail-with-body', + '--silent', + '--show-error', + '--location', + '--retry', '3', + '--retry-delay', '5', + '--retry-all-errors', + '--connect-timeout', '30', + '--max-time', '1800', + '--request', 'PUT', + '--upload-file', path, + '--output', process.platform === 'win32' ? 'NUL' : '/dev/null', + ]; + for (const [name, value] of Object.entries(headers)) { + args.push('--header', `${name}: ${value}`); + } + args.push(url); + + return new Promise((resolveUpload, rejectUpload) => { + const child = spawn('curl', args, { stdio: ['ignore', 'inherit', 'inherit'] }); + child.once('error', rejectUpload); + child.once('exit', (code, signal) => { + if (code === 0) { + resolveUpload(); + } else { + rejectUpload(new Error(`curl upload failed with ${signal ? `signal ${signal}` : `exit code ${code}`}`)); + } + }); + }); +} + export async function publishGitcodeRelease({ directory, repository, @@ -54,6 +88,7 @@ export async function publishGitcodeRelease({ targetCommitish = 'main', prerelease = false, fetchImpl = fetch, + uploadImpl = uploadFileWithCurl, }) { const [owner, repo] = parseRepository(repository); if (!token) throw new Error('GITCODE_ACCESS_TOKEN is required'); @@ -112,15 +147,11 @@ export async function publishGitcodeRelease({ const headers = Array.isArray(rawHeaders) ? Object.fromEntries(rawHeaders.map(({ key, name, value }) => [key ?? name, value])) : rawHeaders && typeof rawHeaders === 'object' ? rawHeaders : {}; - const response = await fetchImpl(uploadUrl, { - method: 'PUT', + await uploadImpl({ + url: uploadUrl, headers, - body: await readFile(join(directory, entry.name)), - redirect: 'follow', + path: join(directory, entry.name), }); - if (!response.ok) { - throw new Error(`Uploading ${entry.name} to GitCode failed: HTTP ${response.status}`); - } console.log(`Uploaded GitCode asset: ${entry.name}`); } } diff --git a/tests/gitcodeRelease.test.ts b/tests/gitcodeRelease.test.ts index 851f9ce..f4f813d 100644 --- a/tests/gitcodeRelease.test.ts +++ b/tests/gitcodeRelease.test.ts @@ -8,6 +8,7 @@ describe('GitCode release mirror', () => { test('creates a release and uploads compiled artifacts', async () => { const directory = await mkdtemp(join(tmpdir(), 'easycli-gitcode-release-')); const calls: Array<{ url: string; method: string; body: unknown }> = []; + const uploads: Array<{ url: string; path: string }> = []; try { await writeFile(join(directory, 'EasyCLIProxyAPI-v1.2.3-Windows-amd64.zip'), 'artifact'); const fetchImpl = async (input: URL | RequestInfo, init: RequestInit = {}) => { @@ -26,9 +27,6 @@ describe('GitCode release mirror', () => { headers: { 'x-upload-token': 'signed' }, }); } - if (method === 'PUT' && url === 'https://file-cdn.gitcode.com/presigned-upload') { - return new Response('', { status: 200 }); - } return new Response('', { status: 500 }); }; @@ -38,13 +36,19 @@ describe('GitCode release mirror', () => { token: 'secret-token', tag: 'v1.2.3', fetchImpl, + uploadImpl: async ({ url, path }) => { + uploads.push({ url, path }); + }, }); - expect(calls.map(({ method }) => method)).toEqual(['GET', 'POST', 'GET', 'PUT']); + expect(calls.map(({ method }) => method)).toEqual(['GET', 'POST', 'GET']); expect(calls[0].url).toContain('/repos/mirror-owner/EasyCLIProxyAPI/releases/tags/v1.2.3'); expect(calls[0].url).toContain('access_token=secret-token'); expect(calls[2].url).toContain('file_name=EasyCLIProxyAPI-v1.2.3-Windows-amd64.zip'); - expect(calls[3].body).toBeInstanceOf(Uint8Array); + expect(uploads).toEqual([{ + url: 'https://file-cdn.gitcode.com/presigned-upload', + path: join(directory, 'EasyCLIProxyAPI-v1.2.3-Windows-amd64.zip'), + }]); } finally { await rm(directory, { recursive: true, force: true }); }