// Publish the built hub binary to a rolling "dev" release on Gitea. // Runs in the CI job with only Node available (the runner image has no // curl/jq/sudo), so this uses Node built-ins + global fetch/FormData/Blob. // // Env: TOKEN (Gitea token), SERVER (github.server_url), REPO (owner/repo), // SHA (github.sha). Expects ./remoterig in the working dir. import { readFileSync } from 'node:fs'; import { createHash } from 'node:crypto'; const { TOKEN, SERVER, REPO, SHA } = process.env; const BIN = 'remoterig'; // Rolling release tag. NOT "dev" — that would collide with the dev branch // and make refs ambiguous (git push/checkout dev breaks). const TAG = 'dev-latest'; const VERSION = SHA.slice(0, 8); const API = `${SERVER}/api/v1/repos/${REPO}`; 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) => { if (!r.ok) throw new Error(`${r.status} ${r.url}\n${await r.text()}`); const t = await r.text(); return t ? JSON.parse(t) : null; }; const bin = readFileSync(BIN); const sha256 = createHash('sha256').update(bin).digest('hex'); const files = { [BIN]: bin, [`${BIN}.sha256`]: Buffer.from(sha256 + '\n'), 'version.txt': Buffer.from(VERSION + '\n'), }; // Roll the release forward to this commit: delete the old release + tag. const existing = await rfetch(`${API}/releases/tags/${TAG}`, { headers: H }); if (existing.ok) { const rel = await existing.json(); await rfetch(`${API}/releases/${rel.id}`, { method: 'DELETE', headers: H }); } await rfetch(`${API}/tags/${TAG}`, { method: 'DELETE', headers: H }); // ignore if absent const rel = await ok(await rfetch(`${API}/releases`, { method: 'POST', headers: { ...H, 'Content-Type': 'application/json' }, body: JSON.stringify({ tag_name: TAG, target_commitish: SHA, name: `${TAG} (${VERSION})`, body: `Rolling dev build ${SHA}`, prerelease: true, }), })); for (const [name, buf] of Object.entries(files)) { const fd = new FormData(); fd.append('attachment', new Blob([buf]), name); await ok(await rfetch(`${API}/releases/${rel.id}/assets?name=${encodeURIComponent(name)}`, { method: 'POST', headers: H, body: fd, })); console.log(`uploaded ${name}`); } console.log(`Published dev release ${VERSION}`);