Release-Script: Asset-Upload mit Retry (transiente Abbrueche bei ~80MB-Assets)
Beim Release v1.7.175 brach der Upload eines ~87MB-Assets transient ab (exit 4). Dank der draft-first-Logik blieb das Release unsichtbar (kein kaputtes Live-Release), musste aber manuell per curl nachgeladen + publiziert werden. Der Upload nutzte einen einmaligen fetch-Stream ohne Retry. Fix: je Asset bis zu 3 Versuche mit Backoff bei Netzwerk-Abbruch oder 5xx; pro Versuch ein frischer createReadStream (konsumierter Stream ist nicht erneut sendbar). 4xx (ausser 409/422 = existiert bereits) brechen weiterhin sofort ab. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f5d435ccd2
commit
da72c11772
@ -279,14 +279,22 @@ async function createOrGetRelease(baseApi, tag, authHeader, notes) {
|
||||
}
|
||||
|
||||
async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, files) {
|
||||
const MAX_ATTEMPTS = 3;
|
||||
for (const fileName of files) {
|
||||
const filePath = path.join(releaseDir, fileName);
|
||||
const fileSize = fs.statSync(filePath).size;
|
||||
const uploadUrl = `${baseApi}/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`;
|
||||
|
||||
// Stream large files instead of loading them entirely into memory
|
||||
// Grosse Assets (~80MB Setup/portable) brechen gelegentlich mitten im Upload ab
|
||||
// (Netzwerk-Reset oder 5xx). Da das Release vorher als Draft angelegt wird, bleibt ein
|
||||
// Fehlschlag hier unsichtbar — aber der Release ist dann unvollstaendig. Deshalb je Asset
|
||||
// bis zu MAX_ATTEMPTS Versuche mit Backoff; ein konsumierter Stream laesst sich nicht
|
||||
// erneut senden, also pro Versuch einen FRISCHEN createReadStream.
|
||||
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const response = await fetch(uploadUrl, {
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
@ -297,6 +305,15 @@ async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, f
|
||||
body: fileStream,
|
||||
duplex: "half"
|
||||
});
|
||||
} catch (error) {
|
||||
fileStream.destroy();
|
||||
if (attempt < MAX_ATTEMPTS) {
|
||||
process.stdout.write(`Upload ${fileName} abgebrochen (Netzwerk, Versuch ${attempt}/${MAX_ATTEMPTS}), neuer Versuch...\n`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000 * attempt));
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Asset upload failed for ${fileName} after ${MAX_ATTEMPTS} attempts: ${String(error?.message || error)}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
let parsed;
|
||||
@ -308,15 +325,22 @@ async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, f
|
||||
|
||||
if (response.ok) {
|
||||
process.stdout.write(`Uploaded: ${fileName}\n`);
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
if (response.status === 409 || response.status === 422) {
|
||||
process.stdout.write(`Skipped existing asset: ${fileName}\n`);
|
||||
break;
|
||||
}
|
||||
// 5xx = transient -> neu versuchen; 4xx (ausser 409/422) = echter Fehler -> abbrechen.
|
||||
if (response.status >= 500 && attempt < MAX_ATTEMPTS) {
|
||||
process.stdout.write(`Upload ${fileName} fehlgeschlagen (${response.status}, Versuch ${attempt}/${MAX_ATTEMPTS}), neuer Versuch...\n`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000 * attempt));
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Asset upload failed for ${fileName} (${response.status}): ${JSON.stringify(parsed)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const rootDir = process.cwd();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user