Support multi-game betas with automatic server selection
This commit is contained in:
@@ -24,6 +24,7 @@ interface DeploymentRow {
|
||||
script_version_id: number | null;
|
||||
prior_version_id: number | null;
|
||||
rolled_back_by: string | null;
|
||||
beta_id: number | null;
|
||||
}
|
||||
|
||||
interface DepScriptRow {
|
||||
@@ -70,7 +71,7 @@ export interface RollbackDeploymentResult {
|
||||
|
||||
export async function rollbackPromoteDeployment(promoteId: string): Promise<RollbackDeploymentResult> {
|
||||
const dep = db()
|
||||
.prepare(`SELECT id, action, script_id, script_version_id, prior_version_id, rolled_back_by
|
||||
.prepare(`SELECT id, action, script_id, script_version_id, prior_version_id, rolled_back_by, beta_id
|
||||
FROM deployments WHERE id = ?`)
|
||||
.get(promoteId) as DeploymentRow | undefined;
|
||||
if (!dep) throw new RollbackError(`no such deployment ${promoteId}`, 'no-such-deployment');
|
||||
@@ -109,9 +110,9 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
)
|
||||
.all(promoteId) as FileRow[];
|
||||
|
||||
// Map rel_path → scriptId (and prior_version_id) so we can restore the
|
||||
// inventory row's script_version_id correctly during file restore.
|
||||
const scriptByTarget = new Map<string, DepScriptRow>();
|
||||
// Different games may promote distinct scripts with the same target path.
|
||||
// Retain all candidates and resolve ownership from the per-server history.
|
||||
const scriptsByTarget = new Map<string, DepScriptRow[]>();
|
||||
const kindByScriptId = new Map<number, ArtifactKind>();
|
||||
if (scripts.length > 0) {
|
||||
const placeholders = scripts.map(() => '?').join(',');
|
||||
@@ -125,11 +126,25 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
}
|
||||
for (const s of scripts) {
|
||||
const t = targetById.get(s.script_id);
|
||||
if (t) scriptByTarget.set(t, s);
|
||||
if (t) scriptsByTarget.set(t, [...(scriptsByTarget.get(t) ?? []), s]);
|
||||
}
|
||||
}
|
||||
const kindForRelPath = (relPath: string): ArtifactKind => {
|
||||
const owner = scriptByTarget.get(relPath);
|
||||
const staged = stagedFileChanges('campaign', promoteId);
|
||||
const scriptForFile = (file: FileRow): DepScriptRow | undefined => {
|
||||
const candidates = scriptsByTarget.get(file.rel_path) ?? [];
|
||||
const versionId = staged.find(row => row.deployment_file_id === file.id)?.script_version_id;
|
||||
if (versionId) return candidates.find(script => script.script_version_id === versionId);
|
||||
if (dep.beta_id) {
|
||||
const participant = candidates.find(script => db().prepare(`SELECT 1 FROM beta_server_scripts
|
||||
WHERE beta_id = ? AND server = ? AND script_id = ?`).get(dep.beta_id, file.server, script.script_id));
|
||||
if (participant) return participant;
|
||||
}
|
||||
return candidates.find(script => getVersion(script.script_version_id)?.sha256 === file.sha256_after) ?? candidates[0];
|
||||
};
|
||||
const kindForFile = (file: FileRow): ArtifactKind => {
|
||||
const stagedKind = staged.find(row => row.deployment_file_id === file.id)?.kind;
|
||||
if (stagedKind) return stagedKind;
|
||||
const owner = scriptForFile(file);
|
||||
if (owner) return owner.kind ?? kindByScriptId.get(owner.script_id) ?? 'script';
|
||||
// Config/baseline aggregate rows have no per-artifact target_rel_path
|
||||
// mapping — the deployment_files row is keyed on the shared cfg name.
|
||||
@@ -144,10 +159,6 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
|
||||
// Group files by server so each server gets exactly one deployment_servers row.
|
||||
const perServer = new Map<string, FileRow[]>();
|
||||
const staged = stagedFileChanges('campaign', promoteId);
|
||||
// Composed baseline rows are physically staged as config files. Prefer the
|
||||
// explicit link instead of inferring their destination from the library.
|
||||
const kindForFile = (file: FileRow) => staged.find(row => row.deployment_file_id === file.id)?.kind ?? kindForRelPath(file.rel_path);
|
||||
for (const f of fileRows) {
|
||||
// A promotion can still be waiting for empty servers. Withdrawing it
|
||||
// must not restore or remove files that it never installed. Rows from
|
||||
@@ -230,8 +241,12 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
putFileBlob(f.content_after),
|
||||
putFileBlob(f.content_before)
|
||||
);
|
||||
const owningScript = scriptByTarget.get(f.rel_path);
|
||||
const priorVersion = owningScript?.prior_version_id ? getVersion(owningScript.prior_version_id) : null;
|
||||
const owningScript = scriptForFile(f);
|
||||
const betaPrior = dep.beta_id && owningScript ? db().prepare(`SELECT prior_version_id FROM beta_server_scripts
|
||||
WHERE beta_id = ? AND server = ? AND script_id = ?`).get(dep.beta_id, server, owningScript.script_id) as
|
||||
{ prior_version_id: number | null } | undefined : undefined;
|
||||
const priorVersionId = betaPrior ? betaPrior.prior_version_id : owningScript?.prior_version_id;
|
||||
const priorVersion = priorVersionId ? getVersion(priorVersionId) : null;
|
||||
queueFileChange({ sourceKind: 'campaign', sourceId: newDepId, server,
|
||||
kind: kindForFile(f), relPath: f.rel_path,
|
||||
content: f.verb === 'added' ? null : f.content_before,
|
||||
|
||||
Reference in New Issue
Block a user