[2026] rollout support, to be short lived I suppose

This commit is contained in:
Devin Zuczek
2026-10-19 22:54:18 -04:00
parent a3291bc9ce
commit 05d7fe4c32
13 changed files with 462 additions and 56 deletions
+16
View File
@@ -17,6 +17,22 @@ export type Env = SharedHonoEnv & {
DOMAIN: string
/** Maximum accepted size of each API-owned image upload, in bytes. */
RECFLARE_MAX_API_UPLOAD_BYTES?: string
/**
* The client builds `/api/versioncheck/v4` answers "current" for, as a comma-separated
* list of builds (`20230414,20250718`), or `off` for none. Unset means the default
* `SUPPORTED_GAME_VERSIONS` in @repo/domain. Set for every worker from the root `.env`
* (`RECFLARE_SUPPORTED_GAME_VERSIONS`), so `auth` refuses a token to exactly the builds
* this flags for an update. Read through `supportedGameBuilds`, which also folds in
* every build under rollout (below).
*/
SUPPORTED_GAME_VERSIONS?: string
/**
* The builds under staged rollout (`RECFLARE_ROLLOUT_GAME_VERSIONS`), same format;
* `off` ends the rollout. Unset means the default `ROLLOUT_GAME_VERSIONS` in
* @repo/domain. A rollout build is always "current" here — it is `auth` that decides who
* may sign in on it — so the version check never sends a supporter to update.
*/
ROLLOUT_GAME_VERSIONS?: string
// Shared rooms database (schema/migrations owned by the `rooms` worker). Used
// read-only here to resolve room roles for `/api/rooms/v1/verifyRole`.
DB: D1Database
+8 -4
View File
@@ -1,7 +1,7 @@
import { Hono } from 'hono'
import { describeRoute } from 'hono-openapi'
import { GAME_VERSION, isSupportedGameVersion } from '@repo/domain'
import { GAME_VERSION, isSupportedGameVersion, supportedGameBuilds } from '@repo/domain'
import { validateAndGetBuild } from '@repo/jwt'
import apiConfigV2 from '../../static/api-config-v2.json'
@@ -106,13 +106,17 @@ export const configRoutes = new Hono<App>({ strict: false })
summary: 'Client version check',
description:
'Whether the client build is current. Compares the client’s `?v=` build against ' +
'the builds we serve (`SUPPORTED_GAME_VERSIONS`): `VersionStatus` is 0 when the ' +
'client is on one of them, 1 when it is on some other build.',
'the builds we serve — the operator’s `SUPPORTED_GAME_VERSIONS` var (or the ' +
'default list in `@repo/domain`) plus every build under rollout ' +
'(`ROLLOUT_GAME_VERSIONS`): `VersionStatus` is 0 when the client is on one of ' +
'them, 1 when it is on some other build. The same lists decide which builds ' +
'`auth` hands a token to, so a build dropped from them is told to update here ' +
'and refused there.',
responses: { 200: json(VersionCheck, 'Version status') },
}),
(c) =>
c.json({
VersionStatus: isSupportedGameVersion(c.req.query('v')) ? 0 : 1,
VersionStatus: isSupportedGameVersion(c.req.query('v'), supportedGameBuilds(c.env)) ? 0 : 1,
UpdateNotificationStage: 0,
IsVersionIslanded: false,
IsCrossPlayDisabled: false,
+50
View File
@@ -323,6 +323,56 @@ describe('public endpoints', () => {
expect(await res.json()).toMatchObject({ VersionStatus: 1 })
})
// The list is the operator's SUPPORTED_GAME_VERSIONS var when set, plus every build
// under rollout (ROLLOUT_GAME_VERSIONS) — the same lists `auth` gates tokens on, so a
// build told to update here is refused there too.
test('GET /api/versioncheck/v4 reads the lists from vars', async () => {
const status = async (version: string) => {
const res = await exports.default.fetch(`${ORIGIN}/api/versioncheck/v4?v=${version}`)
return ((await res.json()) as { VersionStatus: number }).VersionStatus
}
const originals = {
SUPPORTED_GAME_VERSIONS: env.SUPPORTED_GAME_VERSIONS,
ROLLOUT_GAME_VERSIONS: env.ROLLOUT_GAME_VERSIONS,
}
try {
// Forced upgrade: the 2025 build drops out, every point release of it.
env.SUPPORTED_GAME_VERSIONS = '20230414,20260323'
expect(await status('20250718.01')).toBe(1)
expect(await status('20250718')).toBe(1)
expect(await status(GAME_VERSION)).toBe(0)
expect(await status('20260323.14')).toBe(0)
// Alphas not named in the var are gone too: the var replaces the default list.
expect(await status('20231207')).toBe(1)
// A build under rollout is current even when the var omits it: who may sign in on
// it is `auth`'s call, and the version check must not send a supporter to update.
env.SUPPORTED_GAME_VERSIONS = '20230414'
expect(await status('20260323.14')).toBe(0)
env.ROLLOUT_GAME_VERSIONS = 'off'
expect(await status('20260323.14')).toBe(1)
env.ROLLOUT_GAME_VERSIONS = '20250718'
expect(await status('20250718.01')).toBe(0)
// Entries are builds: a copied point release matches, and `off` empties the list.
env.ROLLOUT_GAME_VERSIONS = undefined
env.SUPPORTED_GAME_VERSIONS = '20250718.01'
expect(await status('20250718.2')).toBe(0)
expect(await status(GAME_VERSION)).toBe(1)
env.SUPPORTED_GAME_VERSIONS = 'off'
env.ROLLOUT_GAME_VERSIONS = 'off'
for (const version of SUPPORTED_GAME_VERSIONS) expect(await status(version), version).toBe(1)
// Blank is unset: the default list.
env.SUPPORTED_GAME_VERSIONS = ''
env.ROLLOUT_GAME_VERSIONS = ''
for (const version of SUPPORTED_GAME_VERSIONS) expect(await status(version), version).toBe(0)
} finally {
env.SUPPORTED_GAME_VERSIONS = originals.SUPPORTED_GAME_VERSIONS
env.ROLLOUT_GAME_VERSIONS = originals.ROLLOUT_GAME_VERSIONS
}
})
// Two catalogs, picked off the token's `rn.ver`: a build newer than GAME_VERSION gets
// the 2025 one. `Avatars.AdvancedFaceCustomizationEnabled` is a key only that catalog
// has, so its presence identifies which body was served.