ci: retry transient network errors when publishing the release
Build (Dev) / build (push) Successful in 11s
CI / quality (push) Successful in 10s
CI / quality (pull_request) Successful in 10s

The publish step died with "fetch failed: ECONNRESET" mid-run, leaving a
half-created release (no version.txt asset → the Pi got 404s). Wrap the
Gitea API calls in a small retry (rfetch) so a flaky connection doesn't
leave the rolling release incomplete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Joshua King
2026-06-05 12:44:38 -04:00
parent 7929d1d969
commit 18db26c265
+20 -5
View File
@@ -16,6 +16,21 @@ const VERSION = SHA.slice(0, 8);
const API = `${SERVER}/api/v1/repos/${REPO}`; const API = `${SERVER}/api/v1/repos/${REPO}`;
const H = { Authorization: `token ${TOKEN}` }; const H = { Authorization: `token ${TOKEN}` };
// The runner's network to Gitea is flaky (ECONNRESET mid-publish leaves a
// half-created release). Retry transient fetch failures so the multi-step
// publish is atomic-enough in practice.
const rfetch = async (url, opts = {}, tries = 4) => {
for (let i = 1; ; i++) {
try {
return await fetch(url, opts);
} catch (e) {
if (i >= tries) throw e;
console.log(`fetch ${url} failed (${e.cause?.code || e.message}); retry ${i}/${tries - 1}`);
await new Promise((r) => setTimeout(r, 1000 * i));
}
}
};
const ok = async (r) => { const ok = async (r) => {
if (!r.ok) throw new Error(`${r.status} ${r.url}\n${await r.text()}`); if (!r.ok) throw new Error(`${r.status} ${r.url}\n${await r.text()}`);
const t = await r.text(); const t = await r.text();
@@ -31,14 +46,14 @@ const files = {
}; };
// Roll the release forward to this commit: delete the old release + tag. // Roll the release forward to this commit: delete the old release + tag.
const existing = await fetch(`${API}/releases/tags/${TAG}`, { headers: H }); const existing = await rfetch(`${API}/releases/tags/${TAG}`, { headers: H });
if (existing.ok) { if (existing.ok) {
const rel = await existing.json(); const rel = await existing.json();
await fetch(`${API}/releases/${rel.id}`, { method: 'DELETE', headers: H }); await rfetch(`${API}/releases/${rel.id}`, { method: 'DELETE', headers: H });
} }
await fetch(`${API}/tags/${TAG}`, { method: 'DELETE', headers: H }); // ignore if absent await rfetch(`${API}/tags/${TAG}`, { method: 'DELETE', headers: H }); // ignore if absent
const rel = await ok(await fetch(`${API}/releases`, { const rel = await ok(await rfetch(`${API}/releases`, {
method: 'POST', method: 'POST',
headers: { ...H, 'Content-Type': 'application/json' }, headers: { ...H, 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -53,7 +68,7 @@ const rel = await ok(await fetch(`${API}/releases`, {
for (const [name, buf] of Object.entries(files)) { for (const [name, buf] of Object.entries(files)) {
const fd = new FormData(); const fd = new FormData();
fd.append('attachment', new Blob([buf]), name); fd.append('attachment', new Blob([buf]), name);
await ok(await fetch(`${API}/releases/${rel.id}/assets?name=${encodeURIComponent(name)}`, { await ok(await rfetch(`${API}/releases/${rel.id}/assets?name=${encodeURIComponent(name)}`, {
method: 'POST', headers: H, body: fd, method: 'POST', headers: H, body: fd,
})); }));
console.log(`uploaded ${name}`); console.log(`uploaded ${name}`);