fix(release): stream GitCode asset uploads with curl

This commit is contained in:
lzt404 committed 2026-08-05 17:11:01 +08:00
1 parent 06f024abde
commit ff1ac4ca5a
2 files changed
+48 -13

No files matched your search

+39 -8
View File
@@ -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}`);
}
}
+9 -5
View File
@@ -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 });
}