Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions packages/codev-sandbox/script/publish-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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).")
Expand Down
15 changes: 15 additions & 0 deletions packages/codev-sandbox/src/const.test.ts
Original file line number Diff line number Diff line change
@@ -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",
)
})
11 changes: 11 additions & 0 deletions packages/codev-sandbox/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Loading