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 1/2] 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 }); } From d679f8983fefdb885bc2d015d42baac8f4c5648d Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Wed, 5 Aug 2026 17:15:40 +0800 Subject: [PATCH 2/2] ci(release): isolate GitCode mirroring workflow --- .github/workflows/gitcode-mirror.yml | 84 ++++++++++++++++++++++++++++ .github/workflows/release.yml | 14 +---- 2 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/gitcode-mirror.yml diff --git a/.github/workflows/gitcode-mirror.yml b/.github/workflows/gitcode-mirror.yml new file mode 100644 index 0000000..a7cc1c5 --- /dev/null +++ b/.github/workflows/gitcode-mirror.yml @@ -0,0 +1,84 @@ +name: Mirror GitHub release to GitCode + +run-name: >- + GitCode mirror ${{ inputs.release_tag || github.event.workflow_run.display_title }} + +on: + workflow_run: + workflows: + - Build portable releases + types: + - completed + workflow_dispatch: + inputs: + release_tag: + description: Existing GitHub release tag to mirror + required: true + type: string + +permissions: + contents: read + +concurrency: + group: gitcode-mirror-${{ inputs.release_tag || github.event.workflow_run.id }} + cancel-in-progress: false + +jobs: + mirror: + name: Mirror release assets + if: >- + vars.GITCODE_REPOSITORY != '' + && ( + github.event_name == 'workflow_dispatch' + || github.event.workflow_run.conclusion == 'success' + ) + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + - name: Resolve release tag + id: release + shell: bash + env: + INPUT_RELEASE_TAG: ${{ inputs.release_tag }} + RUN_TITLE: ${{ github.event.workflow_run.display_title }} + run: | + set -euo pipefail + + release_tag="$INPUT_RELEASE_TAG" + if [ -z "$release_tag" ]; then + case "$RUN_TITLE" in + "Portable release "*) release_tag="${RUN_TITLE#Portable release }" ;; + *) echo "Cannot resolve a release tag from workflow title: $RUN_TITLE" >&2; exit 1 ;; + esac + fi + case "$release_tag" in + v*) ;; + *) release_tag="v$release_tag" ;; + esac + if ! node scripts/app-version.mjs "${release_tag#v}" >/dev/null 2>&1; then + if [ -n "$INPUT_RELEASE_TAG" ]; then + echo "Invalid release tag: $release_tag" >&2 + exit 1 + fi + echo "Skipping GitCode mirror because the completed run did not publish a release" + echo "should_mirror=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "tag=$release_tag" >> "$GITHUB_OUTPUT" + echo "should_mirror=true" >> "$GITHUB_OUTPUT" + - name: Download GitHub release assets + if: steps.release.outputs.should_mirror == 'true' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release download "${{ steps.release.outputs.tag }}" --dir artifacts + - name: Publish GitCode mirror + if: steps.release.outputs.should_mirror == 'true' + env: + ARTIFACT_DIRECTORY: artifacts + GITCODE_ACCESS_TOKEN: ${{ secrets.GITCODE_ACCESS_TOKEN }} + GITCODE_REPOSITORY: ${{ vars.GITCODE_REPOSITORY }} + GITCODE_TARGET_COMMITISH: ${{ vars.GITCODE_TARGET_COMMITISH || 'main' }} + RELEASE_TAG: ${{ steps.release.outputs.tag }} + run: node scripts/publish-gitcode-release.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index accc965..f6b4e4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -391,7 +391,7 @@ jobs: if-no-files-found: error retention-days: 14 publish: - name: Publish releases + name: Publish GitHub release if: >- (github.event_name == 'release' && github.event.release.prerelease == true) || startsWith(github.ref, 'refs/tags/') @@ -400,7 +400,7 @@ jobs: - version - build runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 15 steps: - uses: actions/checkout@v7 with: @@ -461,13 +461,3 @@ jobs: else gh release create "$RELEASE_TAG" "${release_args[@]}" artifacts/* fi - - name: Publish GitCode mirror - if: ${{ vars.GITCODE_REPOSITORY != '' }} - env: - ARTIFACT_DIRECTORY: artifacts - GITCODE_ACCESS_TOKEN: ${{ secrets.GITCODE_ACCESS_TOKEN }} - GITCODE_REPOSITORY: ${{ vars.GITCODE_REPOSITORY }} - GITCODE_TARGET_COMMITISH: ${{ vars.GITCODE_TARGET_COMMITISH || 'main' }} - PRERELEASE: ${{ github.event_name == 'release' && github.event.release.prerelease || inputs.prerelease }} - RELEASE_TAG: ${{ needs.version.outputs.tag }} - run: node scripts/publish-gitcode-release.mjs