From 500f64c48e40feec47002f006f6105cfcdd4b3ed Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Wed, 19 Aug 2026 11:50:25 +0700 Subject: [PATCH] feat(desktop): mirror each release to a stable bucket prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A download link had to name /codev//, so every link written down anywhere — docs, an email, a chat message — died at the next release. stage-release now also copies each installer to /codev/latest/. Both sides are already in the bucket, so mc does a server-side copy: the mirror costs storage, not a second upload of ~1.8GB. Blockmaps are skipped, since they only mean anything beside the artifact a manifest names, which is the versioned copy. Nothing reads the prefix. The manifests still name / and that is all an installed build resolves, so this cannot affect auto-update. Also stops uploading every artifact once per channel file. The loop uploaded a whole version folder for each manifest, and every manifest of a release carries the same version — so a four-channel release uploaded each artifact four times. Deduping the versions first makes it once. Co-Authored-By: Claude Opus 5 (1M context) --- packages/desktop/scripts/stage-release.ts | 37 +++++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/desktop/scripts/stage-release.ts b/packages/desktop/scripts/stage-release.ts index 9fbe638861..372cb50004 100644 --- a/packages/desktop/scripts/stage-release.ts +++ b/packages/desktop/scripts/stage-release.ts @@ -11,13 +11,22 @@ // setFeedURL). electron-updater resolves the prefixed name with // `new URL(name, base)`, so no client change is needed. // +// Each release is also mirrored to a stable prefix, so a download link can be +// written down once and keep working. That is for humans only: the manifests +// name / and an installed build never resolves anything else. +// // Produces, for base https://…/codev: // /latest-mac.yml -> …/codev/latest-mac.yml // // -> …/codev//… +// -> …/codev/latest/… (copied in-bucket) import { $ } from "bun" import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises" import { join } from "node:path" +// A folder named "latest" beside the latest*.yml files. Confusable on a +// listing, but it is the name a download link is expected to have. +const STABLE_PREFIX = "latest" + const distDir = argValue("--dist") ?? join(import.meta.dir, "..", "dist") const outDir = argValue("--out") ?? join(distDir, "release") @@ -84,14 +93,36 @@ const upload = (from: string, to: string) => ? $`mc cp ${from} ${target}/${to}` : Promise.resolve(console.log(` mc cp ${from} //codev/${to}`)) +// Both sides are already in the bucket, so mc issues a server-side copy and no +// bytes travel through here — the mirror costs storage, not another upload. +const mirror = (from: string, to: string) => + target + ? $`mc cp ${target}/${from} ${target}/${to}` + : Promise.resolve(console.log(` mc cp //codev/${from} //codev/${to}`)) + +// Every manifest of a release carries the same version, and the loop below +// uploads a version folder whole — so without deduping, four channel files mean +// uploading every artifact four times. +const versions = new Set() +for (const manifest of manifests) + versions.add(/^version:\s*(\S+)\s*$/m.exec(await readFile(join(outDir, manifest), "utf8"))![1]!) + if (!target) console.log("\nCODEV_UPDATE_TARGET is unset; run these where the storage credentials live:") // Artifacts first, manifests last: a client that reads the manifest must find // every file it names already in place. -for (const manifest of manifests) { - const version = /^version:\s*(\S+)\s*$/m.exec(await readFile(join(outDir, manifest), "utf8"))![1]! +for (const version of versions) for (const file of await readdir(join(outDir, version))) await upload(join(outDir, version, file), `${version}/${file}`) -} + +// Then the stable prefix. Nothing reads it — the manifests name / and +// that is all an installed build ever resolves — it exists so a download link +// can be written down once, in the docs or an email, and keep working. +// Blockmaps are left out: they only mean anything beside the artifact a +// manifest names, which is the versioned copy. +for (const version of versions) + for (const file of await readdir(join(outDir, version))) + if (!file.endsWith(".blockmap")) await mirror(`${version}/${file}`, `${STABLE_PREFIX}/${file}`) + for (const manifest of manifests) await upload(join(outDir, manifest), manifest) function argValue(flag: string) {