5 files changed
+80
-12
No files matched your search
@@ -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 }}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$BuildJobs
|
||||
[int]$BuildJobs,
|
||||
|
||||
[string]$GitCodeRepository = 'lzt404/EasyCLIProxyAPI'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -25,12 +27,16 @@ if (-not $PSBoundParameters.ContainsKey('BuildJobs')) {
|
||||
if ($BuildJobs -lt 1 -or $BuildJobs -gt 256) {
|
||||
throw 'BuildJobs must be between 1 and 256.'
|
||||
}
|
||||
if ($GitCodeRepository -notmatch '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$') {
|
||||
throw 'GitCodeRepository must use the owner/repository format.'
|
||||
}
|
||||
|
||||
if (-not (Get-Command bun -ErrorAction SilentlyContinue)) {
|
||||
throw 'bun is not installed or not in PATH.'
|
||||
}
|
||||
|
||||
Write-Host "Cargo build jobs: $BuildJobs"
|
||||
Write-Host "GitCode fallback repository: $GitCodeRepository"
|
||||
|
||||
& bun install
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
@@ -38,8 +44,10 @@ if ($LASTEXITCODE -ne 0) {
|
||||
}
|
||||
|
||||
$PreviousBuildJobs = $env:CARGO_BUILD_JOBS
|
||||
$PreviousGitCodeRepository = $env:GITCODE_REPOSITORY
|
||||
try {
|
||||
$env:CARGO_BUILD_JOBS = [string]$BuildJobs
|
||||
$env:GITCODE_REPOSITORY = $GitCodeRepository
|
||||
& bun tauri build --no-bundle
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Tauri build failed with exit code $LASTEXITCODE."
|
||||
@@ -52,6 +60,12 @@ finally {
|
||||
else {
|
||||
$env:CARGO_BUILD_JOBS = $PreviousBuildJobs
|
||||
}
|
||||
if ($null -eq $PreviousGitCodeRepository) {
|
||||
Remove-Item Env:GITCODE_REPOSITORY -ErrorAction SilentlyContinue
|
||||
}
|
||||
else {
|
||||
$env:GITCODE_REPOSITORY = $PreviousGitCodeRepository
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $AppBin -PathType Leaf)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
BUILD_JOBS="${CARGO_BUILD_JOBS:-20}"
|
||||
GITCODE_REPOSITORY="${GITCODE_REPOSITORY:-lzt404/EasyCLIProxyAPI}"
|
||||
APP_BIN="$ROOT_DIR/src-tauri/target/release/cpa-gui"
|
||||
BIN_DIR="$ROOT_DIR/bin-work"
|
||||
BIN_OUT="$BIN_DIR/EasyCLIProxyAPI"
|
||||
@@ -15,8 +16,9 @@ if ! command -v bun >/dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
echo "Cargo build jobs: $BUILD_JOBS"
|
||||
echo "GitCode fallback repository: $GITCODE_REPOSITORY"
|
||||
bun install
|
||||
CARGO_BUILD_JOBS="$BUILD_JOBS" bun tauri build --no-bundle
|
||||
CARGO_BUILD_JOBS="$BUILD_JOBS" GITCODE_REPOSITORY="$GITCODE_REPOSITORY" bun tauri build --no-bundle
|
||||
|
||||
if [ ! -x "$APP_BIN" ]; then
|
||||
echo "Build finished, but executable not found: $APP_BIN"
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user