diff --git a/.github/workflows/gitcode-mirror.yml b/.github/workflows/gitcode-mirror.yml index a7cc1c5..c4ba34c 100644 --- a/.github/workflows/gitcode-mirror.yml +++ b/.github/workflows/gitcode-mirror.yml @@ -33,7 +33,7 @@ jobs: || github.event.workflow_run.conclusion == 'success' ) runs-on: ubuntu-24.04 - timeout-minutes: 60 + timeout-minutes: 35 steps: - uses: actions/checkout@v7 - name: Resolve release tag @@ -75,6 +75,7 @@ jobs: run: gh release download "${{ steps.release.outputs.tag }}" --dir artifacts - name: Publish GitCode mirror if: steps.release.outputs.should_mirror == 'true' + timeout-minutes: 30 env: ARTIFACT_DIRECTORY: artifacts GITCODE_ACCESS_TOKEN: ${{ secrets.GITCODE_ACCESS_TOKEN }} diff --git a/scripts/publish-gitcode-release.mjs b/scripts/publish-gitcode-release.mjs index 3cc245e..d6fb237 100644 --- a/scripts/publish-gitcode-release.mjs +++ b/scripts/publish-gitcode-release.mjs @@ -4,6 +4,8 @@ import { basename, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const API_BASE = 'https://api.gitcode.com/api/v5'; +const API_TIMEOUT_MS = 30_000; +const UPLOAD_TIMEOUT_SECONDS = 180; function parseRepository(value) { const repository = String(value ?? '').trim(); @@ -36,6 +38,7 @@ async function apiRequest(path, { method, headers: body ? { 'Content-Type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined, + signal: AbortSignal.timeout(API_TIMEOUT_MS), }); } catch (error) { throw new Error(`GitCode API request failed for ${path}: ${error instanceof Error ? error.message : error}`); @@ -53,11 +56,10 @@ export function uploadFileWithCurl({ url, headers, path }) { '--silent', '--show-error', '--location', - '--retry', '3', - '--retry-delay', '5', - '--retry-all-errors', '--connect-timeout', '30', - '--max-time', '1800', + '--speed-limit', '1', + '--speed-time', '30', + '--max-time', String(UPLOAD_TIMEOUT_SECONDS), '--request', 'PUT', '--upload-file', path, '--output', process.platform === 'win32' ? 'NUL' : '/dev/null', @@ -147,11 +149,22 @@ export async function publishGitcodeRelease({ const headers = Array.isArray(rawHeaders) ? Object.fromEntries(rawHeaders.map(({ key, name, value }) => [key ?? name, value])) : rawHeaders && typeof rawHeaders === 'object' ? rawHeaders : {}; - await uploadImpl({ - url: uploadUrl, - headers, - path: join(directory, entry.name), - }); + console.log(`Uploading GitCode asset: ${entry.name}`); + try { + await uploadImpl({ + url: uploadUrl, + headers, + path: join(directory, entry.name), + }); + } catch (error) { + console.warn(`GitCode upload command did not finish cleanly; verifying asset: ${entry.name}`); + const refreshedRelease = await apiRequest(releaseByTagPath, { token, fetchImpl }); + const uploadedDespiteError = (refreshedRelease.assets ?? []) + .some((asset) => asset.name === entry.name); + if (!uploadedDespiteError) throw error; + console.log(`GitCode asset exists despite upload error: ${entry.name}`); + } + existingAssets.add(entry.name); console.log(`Uploaded GitCode asset: ${entry.name}`); } } diff --git a/tests/gitcodeRelease.test.ts b/tests/gitcodeRelease.test.ts index f4f813d..c038d28 100644 --- a/tests/gitcodeRelease.test.ts +++ b/tests/gitcodeRelease.test.ts @@ -53,4 +53,42 @@ describe('GitCode release mirror', () => { await rm(directory, { recursive: true, force: true }); } }); + + test('continues when GitCode stores an asset but the upload response times out', async () => { + const directory = await mkdtemp(join(tmpdir(), 'easycli-gitcode-release-')); + const artifactName = 'EasyCLIProxyAPI-v1.2.3-Windows-amd64.zip'; + let releaseRequests = 0; + try { + await writeFile(join(directory, artifactName), 'artifact'); + const fetchImpl = async (input: URL | RequestInfo) => { + const url = String(input); + if (url.includes('/releases/tags/')) { + releaseRequests += 1; + return Response.json({ + tag_name: 'v1.2.3', + assets: releaseRequests === 1 ? [] : [{ name: artifactName }], + }); + } + if (url.includes('/upload_url?')) { + return Response.json({ upload_url: 'https://file-cdn.gitcode.com/presigned-upload' }); + } + return new Response('', { status: 500 }); + }; + + await expect(publishGitcodeRelease({ + directory, + repository: 'mirror-owner/EasyCLIProxyAPI', + token: 'secret-token', + tag: 'v1.2.3', + fetchImpl, + uploadImpl: async () => { + throw new Error('upload response timed out'); + }, + })).resolves.toBeUndefined(); + + expect(releaseRequests).toBe(2); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); });