Skip to content

Commit e655fed

Browse files
committed
fix(updates): reject retired next channel
1 parent ccbc018 commit e655fed

2 files changed

Lines changed: 10 additions & 6 deletions

File tree

services/updates/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ available to roughly half of IPs after three hours and all IPs after six hours.
3434
Eligibility uses the original publication time (`time_created`) and a SHA-256 hash
3535
of the channel and Cloudflare's `CF-Connecting-IP`. Each IP keeps the same rollout
3636
position across releases in that channel. Requests without this header wait for
37-
the full duration. `next` uses the `beta` channel's configuration.
37+
the full duration. The retired `next` channel is not available or configurable.
3838

3939
Until the active release is eligible, callers receive the newest eligible artifact
4040
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
7070
no-store` because responses can depend on the User-Agent.
7171

7272
Version comparison uses semver, normalizing preview run numbers to numeric prerelease
73-
identifiers and historical `next` versions to `beta`. The `/api/next` channel also
74-
resolves to `beta`.
73+
identifiers and historical `next` versions to `beta`. The retired `/api/next`
74+
channel returns 404; use `/api/beta` instead.
7575

7676
Choose a minimum that older clients can install and that can itself consume the active
7777
release. For the CLI package migration, retain a package-aware release published as

services/updates/src/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ export default {
7777
return new Response("Not found", { status: 404 })
7878
}
7979

80-
const resolved = resolveChannel(path[0])
80+
if (path[0] === "next") return json({ error: "Channel not found" }, 404)
81+
const resolved = path[0]
8182
const agent = request.headers.get("User-Agent")?.match(/^opencode\/(?:([^/]+)\/([^/]+)\/cli|(.*))$/)
8283
const current = url.searchParams.get("current") ?? agent?.[2] ?? agent?.[3]
8384
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) {
200201
const duration = typeof input === "string" && input.trim() ? Number(input) : NaN
201202
if (
202203
!validIdentifier(channel) ||
204+
channel === "next" ||
203205
!Number.isFinite(duration) ||
204206
duration < 0 ||
205207
!Number.isFinite(duration * 3_600_000)
@@ -209,7 +211,7 @@ async function configureRollout(request: Request, env: Env, prefix: string) {
209211
await env.DB.prepare(
210212
"INSERT INTO channel_rollout (channel, duration_hours) VALUES (?, ?) ON CONFLICT (channel) DO UPDATE SET duration_hours = excluded.duration_hours",
211213
)
212-
.bind(resolveChannel(channel), duration)
214+
.bind(channel, duration)
213215
.run()
214216
return Response.redirect(new URL(`${prefix}/admin`, request.url), 303)
215217
}
@@ -230,7 +232,8 @@ async function admin(request: Request, env: Env, prefix: string) {
230232
const rollouts = await env.DB.prepare(
231233
`SELECT channels.channel, COALESCE(channel_rollout.duration_hours, 0) AS duration_hours
232234
FROM (SELECT channel FROM artifact UNION SELECT channel FROM channel_rollout) AS channels
233-
LEFT JOIN channel_rollout ON channel_rollout.channel = channels.channel ORDER BY channels.channel`,
235+
LEFT JOIN channel_rollout ON channel_rollout.channel = channels.channel
236+
WHERE channels.channel != 'next' ORDER BY channels.channel`,
234237
).all<{ channel: string; duration_hours: number }>()
235238
const requestedPage = Number.parseInt(url.searchParams.get("page") ?? "1", 10)
236239
const page = Number.isSafeInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1
@@ -453,6 +456,7 @@ function parseArtifact(input: Record<string, unknown>): ArtifactInput | Response
453456
function parseKey(input: Record<string, unknown>): Omit<ArtifactInput, "metadata"> | Response {
454457
if (
455458
!validIdentifier(input.channel) ||
459+
input.channel === "next" ||
456460
!validIdentifier(input.name) ||
457461
!validIdentifier(input.distribution) ||
458462
!validVersion(input.version)

0 commit comments

Comments
 (0)