diff --git a/services/updates/README.md b/services/updates/README.md index fcc59861847..8ef4584f6ce 100644 --- a/services/updates/README.md +++ b/services/updates/README.md @@ -34,7 +34,7 @@ available to roughly half of IPs after three hours and all IPs after six hours. Eligibility uses the original publication time (`time_created`) and a SHA-256 hash of the channel and Cloudflare's `CF-Connecting-IP`. Each IP keeps the same rollout position across releases in that channel. Requests without this header wait for -the full duration. `next` uses the `beta` channel's configuration. +the full duration. The retired `next` channel is not available or configurable. Until the active release is eligible, callers receive the newest eligible artifact published before it, for the same name and distribution. This also handles overlapping @@ -70,8 +70,8 @@ artifact. All three public API paths apply the same selection and use `Cache-Con no-store` because responses can depend on the User-Agent. Version comparison uses semver, normalizing preview run numbers to numeric prerelease -identifiers and historical `next` versions to `beta`. The `/api/next` channel also -resolves to `beta`. +identifiers and historical `next` versions to `beta`. The retired `/api/next` +channel returns 404; use `/api/beta` instead. Choose a minimum that older clients can install and that can itself consume the active release. For the CLI package migration, retain a package-aware release published as diff --git a/services/updates/src/index.ts b/services/updates/src/index.ts index 79e2c884559..e701e482c52 100644 --- a/services/updates/src/index.ts +++ b/services/updates/src/index.ts @@ -77,7 +77,8 @@ export default { return new Response("Not found", { status: 404 }) } - const resolved = resolveChannel(path[0]) + if (path[0] === "next") return json({ error: "Channel not found" }, 404) + const resolved = path[0] const agent = request.headers.get("User-Agent")?.match(/^opencode\/(?:([^/]+)\/([^/]+)\/cli|(.*))$/) const current = url.searchParams.get("current") ?? agent?.[2] ?? agent?.[3] const source = agent?.[1] ?? current?.match(/^v?0\.0\.0-(.+)-\d+(?:\.\d+)?(?:\+.*)?$/)?.[1] @@ -200,6 +201,7 @@ async function configureRollout(request: Request, env: Env, prefix: string) { const duration = typeof input === "string" && input.trim() ? Number(input) : NaN if ( !validIdentifier(channel) || + channel === "next" || !Number.isFinite(duration) || duration < 0 || !Number.isFinite(duration * 3_600_000) @@ -209,7 +211,7 @@ async function configureRollout(request: Request, env: Env, prefix: string) { await env.DB.prepare( "INSERT INTO channel_rollout (channel, duration_hours) VALUES (?, ?) ON CONFLICT (channel) DO UPDATE SET duration_hours = excluded.duration_hours", ) - .bind(resolveChannel(channel), duration) + .bind(channel, duration) .run() return Response.redirect(new URL(`${prefix}/admin`, request.url), 303) } @@ -230,7 +232,8 @@ async function admin(request: Request, env: Env, prefix: string) { const rollouts = await env.DB.prepare( `SELECT channels.channel, COALESCE(channel_rollout.duration_hours, 0) AS duration_hours FROM (SELECT channel FROM artifact UNION SELECT channel FROM channel_rollout) AS channels - LEFT JOIN channel_rollout ON channel_rollout.channel = channels.channel ORDER BY channels.channel`, + LEFT JOIN channel_rollout ON channel_rollout.channel = channels.channel + WHERE channels.channel != 'next' ORDER BY channels.channel`, ).all<{ channel: string; duration_hours: number }>() const requestedPage = Number.parseInt(url.searchParams.get("page") ?? "1", 10) const page = Number.isSafeInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1 @@ -453,6 +456,7 @@ function parseArtifact(input: Record): ArtifactInput | Response function parseKey(input: Record): Omit | Response { if ( !validIdentifier(input.channel) || + input.channel === "next" || !validIdentifier(input.name) || !validIdentifier(input.distribution) || !validVersion(input.version)