Fix scope restoration for promoted scripts

This commit is contained in:
2026-09-18 08:08:17 -04:00
parent 3c49437bf9
commit b2ec01daf5
4 changed files with 100 additions and 13 deletions
+2 -1
View File
@@ -23,6 +23,7 @@ import {
listPromotedOverlaysForScope,
listVersions,
serversMatchingScope,
setScope,
type ScopeType,
type ScriptRow,
type ScriptVersionRow
@@ -1089,6 +1090,7 @@ export function latestVersionForScript(script: ScriptRow): ScriptVersionRow | nu
export function unsetScopeAndDetach(scriptId: number): string[] {
const script = getScript(scriptId);
if (!script) throw new BetaError(`no such script (id=${scriptId})`, 'no-such-script');
setScope({ scriptId, scopeType: null, scopeValue: null });
const detached: string[] = [];
if (script.kind === 'script' || script.kind === 'plugin') {
@@ -1104,6 +1106,5 @@ export function unsetScopeAndDetach(scriptId: number): string[] {
detached.push(server);
}
}
db().prepare('UPDATE scripts SET scope_type = NULL, scope_value = NULL WHERE id = ?').run(scriptId);
return detached;
}
+12 -10
View File
@@ -57,7 +57,6 @@ export class ScriptError extends Error {
| 'path-empty'
| 'no-such-script'
| 'no-such-version'
| 'scope-requires-beta'
| 'invalid-scope'
| 'in-use-current'
| 'in-use-beta'
@@ -290,21 +289,24 @@ export interface SetScopeInput {
scopeValue: string | null;
}
// Setting or changing a scope while the script already has a promoted version
// requires a beta first (per plan §"Beta trigger"). Un-scoping is free.
// Scope edits preserve the promoted version and do not deploy files. An active
// beta must finish first because promotion reads the script's current scope.
export function setScope(input: SetScopeInput): ScriptRow {
const script = getScript(input.scriptId);
if (!script) throw new ScriptError(`no such script (id=${input.scriptId})`, 'no-such-script');
validateScope(input.scopeType, input.scopeValue);
const changed =
input.scopeType !== script.scope_type || (input.scopeValue ?? null) !== (script.scope_value ?? null);
const isSetOrChange = input.scopeType !== null && changed;
if (isSetOrChange && script.current_version_id !== null) {
const activeBeta = db()
.prepare(
`SELECT b.id FROM betas b
JOIN beta_scripts bs ON bs.beta_id = b.id
WHERE bs.script_id = ? AND b.state IN ('running','ready-promote') LIMIT 1`
)
.get(input.scriptId) as { id: number } | undefined;
if (activeBeta) {
throw new ScriptError(
'setting or changing scope requires opening a beta first',
'scope-requires-beta'
`rollback or promote beta ${activeBeta.id} before changing scope`,
'has-active-beta'
);
}
+2 -2
View File
@@ -8,8 +8,8 @@ function parseId(raw: string): number | null {
}
// POST body: { scopeType: 'game' | 'map' | null, scopeValue?: string }
// Setting/changing scope on a script with a current_version_id returns a
// 409 with kind='scope-requires-beta'. Un-scoping detaches inventory so the
// Scope edits preserve the promoted version; changes during an active beta
// return 409 with kind='has-active-beta'. Un-scoping detaches inventory so the
// orphaned files show up as drift on the deployments page for the operator
// to reconcile manually — the same flow as any other drift.
export async function POST({ params, request }: { params: { id: string }; request: Request }) {