From 04e1f8226c870f3677b95ed4d133d35eb2d64417 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Sat, 15 Aug 2026 10:25:40 +0700 Subject: [PATCH] fix(sandbox): publish images to the prefix clients read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bucket moved the sandbox images from codev-code/sandbox to codev/sandbox and #230 repointed SANDBOX_IMAGES_BASE_URL, but publish-image.ts kept its own hardcoded copy of the old prefix. Every upload therefore landed under a path that reads 403, while the manifest.json clients actually fetch was never touched: the publish printed "published sandbox image " and changed nothing. Derive the destinations from the manifest's own asset URLs and SANDBOX_MANIFEST_URL instead, via sandboxStorageObjectKey — the object key is the URL path minus the bucket segment, which CODEV_STORAGE_TARGET already carries. Publishing now goes exactly where the manifest says the artifacts live, so the two sides cannot drift again; the version guard above already pins those URLs to the version being published. Verified with a synthetic out dir: the printed mc commands target codev/sandbox// and codev/sandbox/manifest.json, artifacts before the manifest. Co-Authored-By: Claude Opus 5 (1M context) --- .../codev-sandbox/script/publish-image.ts | 19 +++++++++++++------ packages/codev-sandbox/src/const.test.ts | 15 +++++++++++++++ packages/codev-sandbox/src/const.ts | 11 +++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 packages/codev-sandbox/src/const.test.ts diff --git a/packages/codev-sandbox/script/publish-image.ts b/packages/codev-sandbox/script/publish-image.ts index e84e3e111f..9ba136c2b1 100644 --- a/packages/codev-sandbox/script/publish-image.ts +++ b/packages/codev-sandbox/script/publish-image.ts @@ -11,6 +11,7 @@ import { $ } from "bun" import { readFile, stat } from "node:fs/promises" import { join } from "node:path" +import { SANDBOX_MANIFEST_URL, sandboxStorageObjectKey } from "../src/const" import { manifestImageKey, parseSandboxManifest } from "../src/manifest" const version = argValue("--version") @@ -44,19 +45,25 @@ if (!/^\d+\.\d+\.\d+$/.test(version) && !force) { process.exit(1) } -const files = Object.values(manifest.images).flatMap((asset) => (asset ? [asset.url.split("/").pop()!] : [])) -for (const name of files) { - const file = await stat(join(outDir, name)).catch(() => null) +// Upload to exactly the locations the manifest advertises: the build script +// derived those URLs from SANDBOX_IMAGES_BASE_URL, and the version guard above +// has already pinned them to this version. A hardcoded prefix here is what +// silently published into a path no client fetches. +const artifacts = Object.values(manifest.images).flatMap((asset) => + asset ? [{ name: asset.url.split("/").pop()!, key: sandboxStorageObjectKey(asset.url) }] : [], +) +for (const artifact of artifacts) { + const file = await stat(join(outDir, artifact.name)).catch(() => null) if (!file?.isFile()) { - console.error(`missing artifact ${name} in ${outDir}`) + console.error(`missing artifact ${artifact.name} in ${outDir}`) process.exit(1) } } const target = process.env.CODEV_STORAGE_TARGET const uploads = [ - ...files.map((name) => [join(outDir, name), `codev-code/sandbox/${version}/${name}`] as const), - [join(outDir, "manifest.json"), "codev-code/sandbox/manifest.json"] as const, + ...artifacts.map((artifact) => [join(outDir, artifact.name), artifact.key] as const), + [join(outDir, "manifest.json"), sandboxStorageObjectKey(SANDBOX_MANIFEST_URL)] as const, ] if (!target) { console.log("CODEV_STORAGE_TARGET is not set (mc alias/bucket, e.g. codev/codev-storage).") diff --git a/packages/codev-sandbox/src/const.test.ts b/packages/codev-sandbox/src/const.test.ts new file mode 100644 index 0000000000..9bf85506dd --- /dev/null +++ b/packages/codev-sandbox/src/const.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from "bun:test" +import { SANDBOX_MANIFEST_URL, sandboxStorageObjectKey } from "./const" + +// The invariant that matters: what publish-image.ts uploads to and what a +// client GETs are the same location. Asserting the suffix rather than the +// literal prefix keeps a deliberate bucket move a one-line change here. +test("the publish key is the tail of the url clients read", () => { + expect(SANDBOX_MANIFEST_URL.endsWith(`/${sandboxStorageObjectKey(SANDBOX_MANIFEST_URL)}`)).toBe(true) +}) + +test("drops the bucket segment, keeps the version folder", () => { + expect(sandboxStorageObjectKey("https://storage.test/bucket/codev/sandbox/1.2.3/vz-arm64.tar.gz")).toBe( + "codev/sandbox/1.2.3/vz-arm64.tar.gz", + ) +}) diff --git a/packages/codev-sandbox/src/const.ts b/packages/codev-sandbox/src/const.ts index 13fb78d7cb..eeb531d9bd 100644 --- a/packages/codev-sandbox/src/const.ts +++ b/packages/codev-sandbox/src/const.ts @@ -7,6 +7,17 @@ export const SANDBOX_IMAGES_BASE_URL = `${STORAGE_BASE}/codev/sandbox` export const SANDBOX_MANIFEST_URL = `${SANDBOX_IMAGES_BASE_URL}/manifest.json` +// mc uploads take an object key, not a URL, and CODEV_STORAGE_TARGET already +// carries the alias and bucket — so the key is the URL path minus its first +// segment (the bucket). Deriving the publish destination from the same +// constant clients read is what keeps a prefix move from updating one side +// only: when the bucket went codev-code/sandbox → codev/sandbox the publish +// script kept uploading to the old prefix, which reads 403, so a publish +// appeared to succeed while the manifest clients fetch never changed. +export function sandboxStorageObjectKey(url: string) { + return new URL(url).pathname.split("/").filter(Boolean).slice(1).join("/") +} + // WSL MSI installers mirrored for corporate networks where only the storage // host is whitelisted; the Microsoft `wsl --install` path stays as fallback. export const INSTALLERS_BASE_URL = `${STORAGE_BASE}/installers`