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:
Sucukdeluxe 2026-05-31 21:40:02 +02:00
parent f5d435ccd2
commit da72c11772

View File

@ -279,42 +279,66 @@ async function createOrGetRelease(baseApi, tag, authHeader, notes) {
} }
async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, files) { async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, files) {
const MAX_ATTEMPTS = 3;
for (const fileName of files) { for (const fileName of files) {
const filePath = path.join(releaseDir, fileName); const filePath = path.join(releaseDir, fileName);
const fileSize = fs.statSync(filePath).size; const fileSize = fs.statSync(filePath).size;
const uploadUrl = `${baseApi}/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`; 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
const fileStream = fs.createReadStream(filePath); // (Netzwerk-Reset oder 5xx). Da das Release vorher als Draft angelegt wird, bleibt ein
const response = await fetch(uploadUrl, { // Fehlschlag hier unsichtbar — aber der Release ist dann unvollstaendig. Deshalb je Asset
method: "POST", // bis zu MAX_ATTEMPTS Versuche mit Backoff; ein konsumierter Stream laesst sich nicht
headers: { // erneut senden, also pro Versuch einen FRISCHEN createReadStream.
Accept: "application/json", for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
Authorization: authHeader, const fileStream = fs.createReadStream(filePath);
"Content-Type": "application/octet-stream", let response;
"Content-Length": String(fileSize) try {
}, response = await fetch(uploadUrl, {
body: fileStream, method: "POST",
duplex: "half" headers: {
}); Accept: "application/json",
Authorization: authHeader,
"Content-Type": "application/octet-stream",
"Content-Length": String(fileSize)
},
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(); const text = await response.text();
let parsed; let parsed;
try { try {
parsed = text ? JSON.parse(text) : null; parsed = text ? JSON.parse(text) : null;
} catch { } catch {
parsed = text; parsed = text;
} }
if (response.ok) { if (response.ok) {
process.stdout.write(`Uploaded: ${fileName}\n`); 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)}`);
} }
if (response.status === 409 || response.status === 422) {
process.stdout.write(`Skipped existing asset: ${fileName}\n`);
continue;
}
throw new Error(`Asset upload failed for ${fileName} (${response.status}): ${JSON.stringify(parsed)}`);
} }
} }