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,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)}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user