@cogenta/fleet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/assert.d.ts +9 -0
- package/dist/agent/assert.d.ts.map +1 -0
- package/dist/agent/assert.js +64 -0
- package/dist/agent/assert.js.map +1 -0
- package/dist/agent/build.d.ts +21 -0
- package/dist/agent/build.d.ts.map +1 -0
- package/dist/agent/build.js +43 -0
- package/dist/agent/build.js.map +1 -0
- package/dist/agent/commands.d.ts +39 -0
- package/dist/agent/commands.d.ts.map +1 -0
- package/dist/agent/commands.js +43 -0
- package/dist/agent/commands.js.map +1 -0
- package/dist/agent/rollback-handler.d.ts +28 -0
- package/dist/agent/rollback-handler.d.ts.map +1 -0
- package/dist/agent/rollback-handler.js +52 -0
- package/dist/agent/rollback-handler.js.map +1 -0
- package/dist/agent/sign.d.ts +18 -0
- package/dist/agent/sign.d.ts.map +1 -0
- package/dist/agent/sign.js +19 -0
- package/dist/agent/sign.js.map +1 -0
- package/dist/agent/types.d.ts +86 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +2 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/alerts/alerts.d.ts +42 -0
- package/dist/alerts/alerts.d.ts.map +1 -0
- package/dist/alerts/alerts.js +98 -0
- package/dist/alerts/alerts.js.map +1 -0
- package/dist/alerts/conditions.d.ts +25 -0
- package/dist/alerts/conditions.d.ts.map +1 -0
- package/dist/alerts/conditions.js +28 -0
- package/dist/alerts/conditions.js.map +1 -0
- package/dist/alerts/tables.d.ts +13 -0
- package/dist/alerts/tables.d.ts.map +1 -0
- package/dist/alerts/tables.js +27 -0
- package/dist/alerts/tables.js.map +1 -0
- package/dist/control/commands.d.ts +38 -0
- package/dist/control/commands.d.ts.map +1 -0
- package/dist/control/commands.js +51 -0
- package/dist/control/commands.js.map +1 -0
- package/dist/control/identity.d.ts +13 -0
- package/dist/control/identity.d.ts.map +1 -0
- package/dist/control/identity.js +5 -0
- package/dist/control/identity.js.map +1 -0
- package/dist/control/ingest.d.ts +46 -0
- package/dist/control/ingest.d.ts.map +1 -0
- package/dist/control/ingest.js +56 -0
- package/dist/control/ingest.js.map +1 -0
- package/dist/control/risk.d.ts +66 -0
- package/dist/control/risk.d.ts.map +1 -0
- package/dist/control/risk.js +130 -0
- package/dist/control/risk.js.map +1 -0
- package/dist/control/state.d.ts +19 -0
- package/dist/control/state.d.ts.map +1 -0
- package/dist/control/state.js +67 -0
- package/dist/control/state.js.map +1 -0
- package/dist/control/tables.d.ts +18 -0
- package/dist/control/tables.d.ts.map +1 -0
- package/dist/control/tables.js +58 -0
- package/dist/control/tables.js.map +1 -0
- package/dist/enrollment/store.d.ts +84 -0
- package/dist/enrollment/store.d.ts.map +1 -0
- package/dist/enrollment/store.js +81 -0
- package/dist/enrollment/store.js.map +1 -0
- package/dist/enrollment/tables.d.ts +20 -0
- package/dist/enrollment/tables.d.ts.map +1 -0
- package/dist/enrollment/tables.js +55 -0
- package/dist/enrollment/tables.js.map +1 -0
- package/dist/enrollment/tokens.d.ts +4 -0
- package/dist/enrollment/tokens.d.ts.map +1 -0
- package/dist/enrollment/tokens.js +20 -0
- package/dist/enrollment/tokens.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory/drift.d.ts +73 -0
- package/dist/inventory/drift.d.ts.map +1 -0
- package/dist/inventory/drift.js +91 -0
- package/dist/inventory/drift.js.map +1 -0
- package/dist/reporting/report.d.ts +65 -0
- package/dist/reporting/report.d.ts.map +1 -0
- package/dist/reporting/report.js +137 -0
- package/dist/reporting/report.js.map +1 -0
- package/dist/reporting/schedule.d.ts +10 -0
- package/dist/reporting/schedule.d.ts.map +1 -0
- package/dist/reporting/schedule.js +34 -0
- package/dist/reporting/schedule.js.map +1 -0
- package/dist/reporting/tables.d.ts +11 -0
- package/dist/reporting/tables.d.ts.map +1 -0
- package/dist/reporting/tables.js +22 -0
- package/dist/reporting/tables.js.map +1 -0
- package/dist/rollout/campaign.d.ts +103 -0
- package/dist/rollout/campaign.d.ts.map +1 -0
- package/dist/rollout/campaign.js +258 -0
- package/dist/rollout/campaign.js.map +1 -0
- package/dist/rollout/rollback.d.ts +51 -0
- package/dist/rollout/rollback.d.ts.map +1 -0
- package/dist/rollout/rollback.js +60 -0
- package/dist/rollout/rollback.js.map +1 -0
- package/dist/rollout/tables.d.ts +19 -0
- package/dist/rollout/tables.d.ts.map +1 -0
- package/dist/rollout/tables.js +65 -0
- package/dist/rollout/tables.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
import type { CommandQueueStore } from '../control/commands.js';
|
|
3
|
+
import type { SiteRisk } from '../control/risk.js';
|
|
4
|
+
import type { SiteStateStore } from '../control/state.js';
|
|
5
|
+
import type { InventoryComponentKind } from '../inventory/drift.js';
|
|
6
|
+
/**
|
|
7
|
+
* "## Mises à jour groupées" (`docs/lots/L8-flotte.md`): "1. Sélection d'un
|
|
8
|
+
* site canari. 2. Application, vérification automatique... 3. Si succès,
|
|
9
|
+
* vague suivante — 10%, puis 50%, puis le reste."
|
|
10
|
+
*
|
|
11
|
+
* **The single most important architectural decision in this module**:
|
|
12
|
+
* verification is real but ASYNCHRONOUS. "Le plan de contrôle n'ouvre jamais
|
|
13
|
+
* de connexion [entrante] vers un site" is absolute, since task 1 — a
|
|
14
|
+
* synchronous HTTP probe from the control plane to a site would violate it.
|
|
15
|
+
* "Le site répond, les pages clés se rendent" is instead learned from the
|
|
16
|
+
* site's own NEXT scheduled contact (its regular telemetry push, tasks 2/3's
|
|
17
|
+
* real ingestion path): if a site keeps contacting the control plane after
|
|
18
|
+
* an update was dispatched to it, and its self-reported inventory
|
|
19
|
+
* (`../inventory/drift.js`'s `extractInventory`) now shows the target
|
|
20
|
+
* version, that IS the verification signal — a real property observable
|
|
21
|
+
* without the control plane ever reaching toward the site. No new
|
|
22
|
+
* verification transport is built here; this module only interprets
|
|
23
|
+
* signals tasks 2/3 already produce.
|
|
24
|
+
*/
|
|
25
|
+
export type CampaignWaveLabel = 'canary' | '10%' | '50%' | 'rest';
|
|
26
|
+
export interface CampaignWave {
|
|
27
|
+
readonly index: number;
|
|
28
|
+
readonly label: CampaignWaveLabel;
|
|
29
|
+
readonly siteIds: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
export type CampaignStatus = 'in_progress' | 'succeeded' | 'halted';
|
|
32
|
+
export type SiteRolloutStatus = 'dispatched' | 'succeeded' | 'failed';
|
|
33
|
+
export interface SiteRolloutRecord {
|
|
34
|
+
readonly siteId: string;
|
|
35
|
+
readonly waveIndex: number;
|
|
36
|
+
readonly status: SiteRolloutStatus;
|
|
37
|
+
/** The component's version this site reported just before the update was dispatched to it — what a rollback (task 8) restores. `null` when the site had never reported the component at all. */
|
|
38
|
+
readonly preUpdateVersion: string | null;
|
|
39
|
+
readonly dispatchedAt: string;
|
|
40
|
+
readonly resolvedAt: string | null;
|
|
41
|
+
}
|
|
42
|
+
export interface CampaignRecord {
|
|
43
|
+
readonly id: string;
|
|
44
|
+
readonly componentKind: InventoryComponentKind;
|
|
45
|
+
readonly componentName: string;
|
|
46
|
+
readonly targetVersion: string;
|
|
47
|
+
readonly waves: readonly CampaignWave[];
|
|
48
|
+
readonly currentWaveIndex: number;
|
|
49
|
+
readonly status: CampaignStatus;
|
|
50
|
+
readonly haltedReason: string | null;
|
|
51
|
+
readonly createdAt: string;
|
|
52
|
+
/** Sites a failed wave flagged for rollback — real, actionable state task 8's rollback execution consumes. Never populated except by a genuine wave failure. */
|
|
53
|
+
readonly failedSiteIds: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Partitions an already-ordered site-id list into the lot's real four waves.
|
|
57
|
+
* `orderedSiteIds[0]` is the canary by construction — ordering (who goes
|
|
58
|
+
* first) is `orderSitesForCanary`'s job, not this function's; this function
|
|
59
|
+
* only turns "an order" into "wave membership," a real, pure, deterministic
|
|
60
|
+
* split. "10%"/"50%" are read as percentages of the whole fleet (rounded up,
|
|
61
|
+
* minimum one site so a small fleet still gets a real staged rollout rather
|
|
62
|
+
* than one giant wave), each wave's membership disjoint from the ones before
|
|
63
|
+
* it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function planWaves(orderedSiteIds: readonly string[]): readonly CampaignWave[];
|
|
66
|
+
/**
|
|
67
|
+
* Real canary-selection rule: the LOWEST-risk site goes first — the safest
|
|
68
|
+
* real choice available, reusing `../control/risk.js`'s already-real
|
|
69
|
+
* scoring rather than an arbitrary pick. A site with no matching risk entry
|
|
70
|
+
* sorts last (never chosen as canary) — an unscored site is not a safe
|
|
71
|
+
* default canary. Ties break on site name for a fully deterministic order.
|
|
72
|
+
*/
|
|
73
|
+
export declare function orderSitesForCanary(siteIds: readonly string[], risks: readonly SiteRisk[]): readonly string[];
|
|
74
|
+
export interface StartCampaignInput {
|
|
75
|
+
readonly siteIds: readonly string[];
|
|
76
|
+
readonly risks: readonly SiteRisk[];
|
|
77
|
+
readonly componentKind: InventoryComponentKind;
|
|
78
|
+
readonly componentName: string;
|
|
79
|
+
readonly targetVersion: string;
|
|
80
|
+
}
|
|
81
|
+
export interface RolloutCampaignStore {
|
|
82
|
+
startCampaign(input: StartCampaignInput): Promise<CampaignRecord>;
|
|
83
|
+
/**
|
|
84
|
+
* Re-evaluates the campaign's current wave against real, already-ingested
|
|
85
|
+
* telemetry (never probes a site) — advances to the next wave on full
|
|
86
|
+
* success, halts on any real failure or timeout, or is a no-op if nothing
|
|
87
|
+
* new has happened yet. Idempotent: calling this repeatedly against an
|
|
88
|
+
* unchanged state re-derives the same result rather than double-advancing.
|
|
89
|
+
*/
|
|
90
|
+
checkProgress(campaignId: string, now?: () => number, timeoutMs?: number): Promise<CampaignRecord>;
|
|
91
|
+
/** Reloads a campaign from real persisted storage — proves campaign state survives a control-plane restart. */
|
|
92
|
+
getCampaign(campaignId: string): Promise<CampaignRecord | null>;
|
|
93
|
+
/**
|
|
94
|
+
* Every real rollout record for one site within one campaign (task 8's
|
|
95
|
+
* real version-history read path) — ordered oldest wave first. A site
|
|
96
|
+
* only ever appears in one wave per campaign, so this is normally a
|
|
97
|
+
* single-element list, but the shape stays an array rather than assuming
|
|
98
|
+
* that never changes.
|
|
99
|
+
*/
|
|
100
|
+
getSiteRolloutRecords(campaignId: string, siteId: string): Promise<readonly SiteRolloutRecord[]>;
|
|
101
|
+
}
|
|
102
|
+
export declare function createRolloutCampaignStore(db: DatabaseHandle, commandQueueStore: CommandQueueStore, stateStore: SiteStateStore, now?: () => number): RolloutCampaignStore;
|
|
103
|
+
//# sourceMappingURL=campaign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"campaign.d.ts","sourceRoot":"","sources":["../../src/rollout/campaign.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { CogentaError, identifier, newId, sql } from '@cogenta/core';
|
|
2
|
+
import { compareVersions, parseVersion } from '@cogenta/plugins';
|
|
3
|
+
import { extractInventory } from '../inventory/drift.js';
|
|
4
|
+
import { ROLLOUT_TABLES } from './tables.js';
|
|
5
|
+
/**
|
|
6
|
+
* "Un échec arrête toute la campagne" — waves 10%/50%/rest of a campaign
|
|
7
|
+
* halted after wave N never receive an `update` command at all (not "receive
|
|
8
|
+
* one but are told to ignore it"): `startCampaign`/`checkProgress` below only
|
|
9
|
+
* ever call `commandQueueStore.enqueue` for the wave they are actively
|
|
10
|
+
* dispatching, and a halted campaign's `checkProgress` is a real no-op.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 24 * 60 * 60 * 1000; // one real daily contact cycle, matching the retention/cadence reasoning in `../control/state.ts`
|
|
13
|
+
/**
|
|
14
|
+
* Partitions an already-ordered site-id list into the lot's real four waves.
|
|
15
|
+
* `orderedSiteIds[0]` is the canary by construction — ordering (who goes
|
|
16
|
+
* first) is `orderSitesForCanary`'s job, not this function's; this function
|
|
17
|
+
* only turns "an order" into "wave membership," a real, pure, deterministic
|
|
18
|
+
* split. "10%"/"50%" are read as percentages of the whole fleet (rounded up,
|
|
19
|
+
* minimum one site so a small fleet still gets a real staged rollout rather
|
|
20
|
+
* than one giant wave), each wave's membership disjoint from the ones before
|
|
21
|
+
* it.
|
|
22
|
+
*/
|
|
23
|
+
export function planWaves(orderedSiteIds) {
|
|
24
|
+
const total = orderedSiteIds.length;
|
|
25
|
+
if (total === 0)
|
|
26
|
+
return [];
|
|
27
|
+
const waves = [];
|
|
28
|
+
let cursor = 0;
|
|
29
|
+
const canary = orderedSiteIds.slice(cursor, cursor + 1);
|
|
30
|
+
cursor += canary.length;
|
|
31
|
+
waves.push({ index: 0, label: 'canary', siteIds: canary });
|
|
32
|
+
if (cursor >= total)
|
|
33
|
+
return waves;
|
|
34
|
+
const tenPercentCount = Math.max(1, Math.ceil(total * 0.1));
|
|
35
|
+
const wave2End = Math.min(total, cursor + tenPercentCount);
|
|
36
|
+
const wave2 = orderedSiteIds.slice(cursor, wave2End);
|
|
37
|
+
cursor = wave2End;
|
|
38
|
+
if (wave2.length > 0)
|
|
39
|
+
waves.push({ index: waves.length, label: '10%', siteIds: wave2 });
|
|
40
|
+
if (cursor >= total)
|
|
41
|
+
return waves;
|
|
42
|
+
const fiftyPercentCount = Math.max(1, Math.ceil(total * 0.5));
|
|
43
|
+
const wave3End = Math.min(total, cursor + fiftyPercentCount);
|
|
44
|
+
const wave3 = orderedSiteIds.slice(cursor, wave3End);
|
|
45
|
+
cursor = wave3End;
|
|
46
|
+
if (wave3.length > 0)
|
|
47
|
+
waves.push({ index: waves.length, label: '50%', siteIds: wave3 });
|
|
48
|
+
if (cursor >= total)
|
|
49
|
+
return waves;
|
|
50
|
+
const rest = orderedSiteIds.slice(cursor);
|
|
51
|
+
if (rest.length > 0)
|
|
52
|
+
waves.push({ index: waves.length, label: 'rest', siteIds: rest });
|
|
53
|
+
return waves;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Real canary-selection rule: the LOWEST-risk site goes first — the safest
|
|
57
|
+
* real choice available, reusing `../control/risk.js`'s already-real
|
|
58
|
+
* scoring rather than an arbitrary pick. A site with no matching risk entry
|
|
59
|
+
* sorts last (never chosen as canary) — an unscored site is not a safe
|
|
60
|
+
* default canary. Ties break on site name for a fully deterministic order.
|
|
61
|
+
*/
|
|
62
|
+
export function orderSitesForCanary(siteIds, risks) {
|
|
63
|
+
const byId = new Map(risks.map((risk) => [risk.siteId, risk]));
|
|
64
|
+
return [...siteIds].sort((a, b) => {
|
|
65
|
+
const riskA = byId.get(a);
|
|
66
|
+
const riskB = byId.get(b);
|
|
67
|
+
const scoreA = riskA?.score ?? Number.POSITIVE_INFINITY;
|
|
68
|
+
const scoreB = riskB?.score ?? Number.POSITIVE_INFINITY;
|
|
69
|
+
if (scoreA !== scoreB)
|
|
70
|
+
return scoreA - scoreB;
|
|
71
|
+
const nameA = riskA?.siteName ?? a;
|
|
72
|
+
const nameB = riskB?.siteName ?? b;
|
|
73
|
+
return nameA.localeCompare(nameB);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function campaignNotFound(campaignId) {
|
|
77
|
+
throw new CogentaError({
|
|
78
|
+
code: 'FLEET_CAMPAIGN_NOT_FOUND',
|
|
79
|
+
message: `No rollout campaign with id "${campaignId}".`,
|
|
80
|
+
hint: 'Check the campaign id, or call startCampaign first.',
|
|
81
|
+
details: { campaignId },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** An internal invariant violation — a campaign row this store itself just wrote is unreadable a moment later. Never expected; a real, typed error rather than a bare throw if it somehow happens. */
|
|
85
|
+
function campaignStateCorrupt(campaignId, when) {
|
|
86
|
+
throw new CogentaError({
|
|
87
|
+
code: 'FLEET_CAMPAIGN_STATE_CORRUPT',
|
|
88
|
+
message: `Campaign "${campaignId}" could not be reloaded ${when} — its own row is missing.`,
|
|
89
|
+
hint: 'This indicates a storage-layer bug, not a caller error.',
|
|
90
|
+
details: { campaignId, when },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function versionReached(reported, target) {
|
|
94
|
+
if (reported === target)
|
|
95
|
+
return true;
|
|
96
|
+
const reportedParsed = parseVersion(reported);
|
|
97
|
+
const targetParsed = parseVersion(target);
|
|
98
|
+
if (reportedParsed === null || targetParsed === null)
|
|
99
|
+
return false;
|
|
100
|
+
return compareVersions(reportedParsed, targetParsed) >= 0;
|
|
101
|
+
}
|
|
102
|
+
export function createRolloutCampaignStore(db, commandQueueStore, stateStore, now = Date.now) {
|
|
103
|
+
const campaigns = identifier(ROLLOUT_TABLES.campaigns, db.dialect);
|
|
104
|
+
const siteStatus = identifier(ROLLOUT_TABLES.siteStatus, db.dialect);
|
|
105
|
+
async function componentVersionOf(siteId, kind, name) {
|
|
106
|
+
const snapshot = await stateStore.latest(siteId);
|
|
107
|
+
if (snapshot === null)
|
|
108
|
+
return null;
|
|
109
|
+
const inventory = extractInventory(snapshot);
|
|
110
|
+
const component = inventory.components.find((c) => c.kind === kind && c.name === name);
|
|
111
|
+
return component?.version ?? null;
|
|
112
|
+
}
|
|
113
|
+
async function dispatchWave(campaignId, wave, componentKind, componentName, targetVersion) {
|
|
114
|
+
const dispatchedAt = new Date(now()).toISOString();
|
|
115
|
+
for (const siteId of wave.siteIds) {
|
|
116
|
+
const preUpdateVersion = await componentVersionOf(siteId, componentKind, componentName);
|
|
117
|
+
await commandQueueStore.enqueue(siteId, 'update', {
|
|
118
|
+
componentKind,
|
|
119
|
+
componentName,
|
|
120
|
+
targetVersion,
|
|
121
|
+
});
|
|
122
|
+
await db.query(sql `
|
|
123
|
+
insert into ${siteStatus}
|
|
124
|
+
(id, campaign_id, site_id, wave_index, status, pre_update_version, dispatched_at, resolved_at)
|
|
125
|
+
values
|
|
126
|
+
(${newId(now)}, ${campaignId}, ${siteId}, ${wave.index}, ${'dispatched'}, ${preUpdateVersion}, ${dispatchedAt}, ${null})`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function loadCampaign(campaignId) {
|
|
130
|
+
const result = await db.query(sql `select id, component_kind, component_name, target_version, waves_json, current_wave_index, status, halted_reason, created_at from ${campaigns} where id = ${campaignId}`);
|
|
131
|
+
const row = result.rows[0];
|
|
132
|
+
if (row === undefined)
|
|
133
|
+
return null;
|
|
134
|
+
const failedRows = await db.query(sql `select distinct site_id from ${siteStatus} where campaign_id = ${campaignId} and status = ${'failed'}`);
|
|
135
|
+
return {
|
|
136
|
+
id: row.id,
|
|
137
|
+
componentKind: row.component_kind,
|
|
138
|
+
componentName: row.component_name,
|
|
139
|
+
targetVersion: row.target_version,
|
|
140
|
+
waves: JSON.parse(row.waves_json),
|
|
141
|
+
currentWaveIndex: Number(row.current_wave_index),
|
|
142
|
+
status: row.status,
|
|
143
|
+
haltedReason: row.halted_reason,
|
|
144
|
+
createdAt: row.created_at,
|
|
145
|
+
failedSiteIds: failedRows.rows.map((r) => r.site_id),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
async startCampaign(input) {
|
|
150
|
+
const orderedIds = orderSitesForCanary(input.siteIds, input.risks);
|
|
151
|
+
const waves = planWaves(orderedIds);
|
|
152
|
+
const id = newId(now);
|
|
153
|
+
const createdAt = new Date(now()).toISOString();
|
|
154
|
+
await db.query(sql `
|
|
155
|
+
insert into ${campaigns}
|
|
156
|
+
(id, component_kind, component_name, target_version, waves_json, current_wave_index, status, halted_reason, created_at)
|
|
157
|
+
values
|
|
158
|
+
(${id}, ${input.componentKind}, ${input.componentName}, ${input.targetVersion}, ${JSON.stringify(waves)}, ${0}, ${'in_progress'}, ${null}, ${createdAt})`);
|
|
159
|
+
const firstWave = waves[0];
|
|
160
|
+
if (firstWave !== undefined) {
|
|
161
|
+
await dispatchWave(id, firstWave, input.componentKind, input.componentName, input.targetVersion);
|
|
162
|
+
}
|
|
163
|
+
const campaign = await loadCampaign(id);
|
|
164
|
+
if (campaign === null)
|
|
165
|
+
return campaignStateCorrupt(id, 'immediately after insert');
|
|
166
|
+
return campaign;
|
|
167
|
+
},
|
|
168
|
+
async checkProgress(campaignId, clock = now, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
169
|
+
const campaign = await loadCampaign(campaignId);
|
|
170
|
+
if (campaign === null)
|
|
171
|
+
return campaignNotFound(campaignId);
|
|
172
|
+
if (campaign.status !== 'in_progress')
|
|
173
|
+
return campaign;
|
|
174
|
+
const currentWave = campaign.waves[campaign.currentWaveIndex];
|
|
175
|
+
if (currentWave === undefined)
|
|
176
|
+
return campaign;
|
|
177
|
+
const rows = await db.query(sql `select id, campaign_id, site_id, wave_index, status, pre_update_version, dispatched_at, resolved_at from ${siteStatus} where campaign_id = ${campaignId} and wave_index = ${currentWave.index}`);
|
|
178
|
+
let anyFailed = false;
|
|
179
|
+
let allResolved = true;
|
|
180
|
+
for (const row of rows.rows) {
|
|
181
|
+
if (row.status !== 'dispatched') {
|
|
182
|
+
if (row.status === 'failed')
|
|
183
|
+
anyFailed = true;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const snapshot = await stateStore.latest(row.site_id);
|
|
187
|
+
const dispatchedAt = row.dispatched_at === null ? 0 : new Date(row.dispatched_at).getTime();
|
|
188
|
+
if (snapshot !== null && new Date(snapshot.ingestedAt).getTime() > dispatchedAt) {
|
|
189
|
+
const inventory = extractInventory(snapshot);
|
|
190
|
+
const component = inventory.components.find((c) => c.kind === campaign.componentKind && c.name === campaign.componentName);
|
|
191
|
+
const reached = component !== undefined && versionReached(component.version, campaign.targetVersion);
|
|
192
|
+
const resolvedAt = new Date(clock()).toISOString();
|
|
193
|
+
const status = reached ? 'succeeded' : 'failed';
|
|
194
|
+
await db.query(sql `update ${siteStatus} set status = ${status}, resolved_at = ${resolvedAt} where id = ${row.id}`);
|
|
195
|
+
if (!reached)
|
|
196
|
+
anyFailed = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
// No new, post-dispatch telemetry yet — inconclusive. A real,
|
|
200
|
+
// bounded timeout also counts as failure: a site that never checks
|
|
201
|
+
// back in is exactly as unsafe to advance past as one that reports
|
|
202
|
+
// a real problem.
|
|
203
|
+
if (clock() - dispatchedAt > timeoutMs) {
|
|
204
|
+
const resolvedAt = new Date(clock()).toISOString();
|
|
205
|
+
await db.query(sql `update ${siteStatus} set status = ${'failed'}, resolved_at = ${resolvedAt} where id = ${row.id}`);
|
|
206
|
+
anyFailed = true;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
allResolved = false;
|
|
210
|
+
}
|
|
211
|
+
if (anyFailed) {
|
|
212
|
+
await db.query(sql `
|
|
213
|
+
update ${campaigns} set status = ${'halted'}, halted_reason = ${`wave "${currentWave.label}" reported a failure`}
|
|
214
|
+
where id = ${campaignId}`);
|
|
215
|
+
const halted = await loadCampaign(campaignId);
|
|
216
|
+
if (halted === null)
|
|
217
|
+
return campaignStateCorrupt(campaignId, 'during halt');
|
|
218
|
+
return halted;
|
|
219
|
+
}
|
|
220
|
+
if (!allResolved) {
|
|
221
|
+
const stillInProgress = await loadCampaign(campaignId);
|
|
222
|
+
if (stillInProgress === null)
|
|
223
|
+
return campaignStateCorrupt(campaignId, 'mid-check');
|
|
224
|
+
return stillInProgress;
|
|
225
|
+
}
|
|
226
|
+
// Every site in the current wave succeeded — advance, or finish.
|
|
227
|
+
const nextIndex = campaign.currentWaveIndex + 1;
|
|
228
|
+
const nextWave = campaign.waves[nextIndex];
|
|
229
|
+
if (nextWave === undefined) {
|
|
230
|
+
await db.query(sql `update ${campaigns} set status = ${'succeeded'} where id = ${campaignId}`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
await db.query(sql `update ${campaigns} set current_wave_index = ${nextIndex} where id = ${campaignId}`);
|
|
234
|
+
await dispatchWave(campaignId, nextWave, campaign.componentKind, campaign.componentName, campaign.targetVersion);
|
|
235
|
+
}
|
|
236
|
+
const advanced = await loadCampaign(campaignId);
|
|
237
|
+
if (advanced === null)
|
|
238
|
+
return campaignStateCorrupt(campaignId, 'after advancing');
|
|
239
|
+
return advanced;
|
|
240
|
+
},
|
|
241
|
+
getCampaign: loadCampaign,
|
|
242
|
+
async getSiteRolloutRecords(campaignId, siteId) {
|
|
243
|
+
const rows = await db.query(sql `
|
|
244
|
+
select id, campaign_id, site_id, wave_index, status, pre_update_version, dispatched_at, resolved_at
|
|
245
|
+
from ${siteStatus} where campaign_id = ${campaignId} and site_id = ${siteId}
|
|
246
|
+
order by wave_index asc`);
|
|
247
|
+
return rows.rows.map((row) => ({
|
|
248
|
+
siteId: row.site_id,
|
|
249
|
+
waveIndex: row.wave_index,
|
|
250
|
+
status: row.status,
|
|
251
|
+
preUpdateVersion: row.pre_update_version,
|
|
252
|
+
dispatchedAt: row.dispatched_at ?? '',
|
|
253
|
+
resolvedAt: row.resolved_at,
|
|
254
|
+
}));
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=campaign.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"campaign.js","sourceRoot":"","sources":["../../src/rollout/campaign.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { CommandQueueStore, FleetCommand } from '../control/commands.js';
|
|
2
|
+
import type { InventoryComponentKind } from '../inventory/drift.js';
|
|
3
|
+
import type { CampaignRecord, RolloutCampaignStore } from './campaign.js';
|
|
4
|
+
/**
|
|
5
|
+
* "## Mises à jour groupées" (`docs/lots/L8-flotte.md`): "Un échec arrête
|
|
6
|
+
* toute la campagne et **propose** le retour arrière du site concerné."
|
|
7
|
+
*
|
|
8
|
+
* "Propose", not "déclenche" — a halted campaign never auto-enqueues a
|
|
9
|
+
* rollback by itself. An unattended automatic rollback of what might be a
|
|
10
|
+
* transient contact failure (task 7's `checkProgress` treats "no telemetry
|
|
11
|
+
* within the timeout" the same as a real reported failure, and a real
|
|
12
|
+
* network hiccup is a legitimate transient cause) is a real operational risk
|
|
13
|
+
* the lot's own words don't ask for. `listRollbackCandidates` below is the
|
|
14
|
+
* "propose" half — real, queryable data, never an action by itself.
|
|
15
|
+
* `triggerRollback` is the real, separate act a human (or, later, some
|
|
16
|
+
* explicit automation a deployer opts into) performs deliberately.
|
|
17
|
+
*/
|
|
18
|
+
export interface RollbackCandidate {
|
|
19
|
+
readonly siteId: string;
|
|
20
|
+
readonly componentKind: InventoryComponentKind;
|
|
21
|
+
readonly componentName: string;
|
|
22
|
+
/** What this site reported for the component just before the campaign updated it — `null` when the site had never reported it at all, in which case there is nothing real to roll back to. */
|
|
23
|
+
readonly rollbackToVersion: string | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Real rollback candidates for a halted campaign — one entry per site the
|
|
27
|
+
* campaign's own real wave-verification flagged as failed
|
|
28
|
+
* (`CampaignRecord.failedSiteIds`, task 7), each carrying the real version
|
|
29
|
+
* that same site reported immediately before this campaign touched it
|
|
30
|
+
* (`SiteRolloutRecord.preUpdateVersion`, task 7's own per-site history — not
|
|
31
|
+
* `../control/state.js`'s general drift-tracking history, which has no
|
|
32
|
+
* concept of "before THIS campaign"). Returns an empty list for a campaign
|
|
33
|
+
* that is not halted — there is nothing to propose while it is still
|
|
34
|
+
* running or after it fully succeeded.
|
|
35
|
+
*/
|
|
36
|
+
export declare function listRollbackCandidates(campaignStore: RolloutCampaignStore, campaign: CampaignRecord): Promise<readonly RollbackCandidate[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Enqueues a real, signed `rollback` command (task 6's real command-queue
|
|
39
|
+
* primitive) for exactly one site — strictly per-site, the same isolation
|
|
40
|
+
* discipline every prior L8 task has held: there is no "roll back the whole
|
|
41
|
+
* fleet" operation, matching "## Mises à jour groupées"'s own words
|
|
42
|
+
* verbatim: "Chaque site conserve son propre historique de version et peut
|
|
43
|
+
* être revenu en arrière indépendamment. Il n'existe pas d'état global à
|
|
44
|
+
* restaurer." Callable standalone, independent of any campaign — an
|
|
45
|
+
* operator deciding a specific site needs to go back does not need a
|
|
46
|
+
* campaign to exist at all; `listRollbackCandidates` above is one real way
|
|
47
|
+
* to arrive at the right `(siteId, componentKind, componentName, version)`
|
|
48
|
+
* tuple, not the only way to call this.
|
|
49
|
+
*/
|
|
50
|
+
export declare function triggerRollback(commandQueueStore: CommandQueueStore, siteId: string, componentKind: InventoryComponentKind, componentName: string, rollbackToVersion: string | null): Promise<FleetCommand>;
|
|
51
|
+
//# sourceMappingURL=rollback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rollback.d.ts","sourceRoot":"","sources":["../../src/rollout/rollback.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAC7E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAEzE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,aAAa,EAAE,sBAAsB,CAAA;IAC9C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,8LAA8L;IAC9L,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1C;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAC1C,aAAa,EAAE,oBAAoB,EACnC,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAkBvC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,iBAAiB,EAAE,iBAAiB,EACpC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,sBAAsB,EACrC,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAC/B,OAAO,CAAC,YAAY,CAAC,CAcvB"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CogentaError } from '@cogenta/core';
|
|
2
|
+
/**
|
|
3
|
+
* Real rollback candidates for a halted campaign — one entry per site the
|
|
4
|
+
* campaign's own real wave-verification flagged as failed
|
|
5
|
+
* (`CampaignRecord.failedSiteIds`, task 7), each carrying the real version
|
|
6
|
+
* that same site reported immediately before this campaign touched it
|
|
7
|
+
* (`SiteRolloutRecord.preUpdateVersion`, task 7's own per-site history — not
|
|
8
|
+
* `../control/state.js`'s general drift-tracking history, which has no
|
|
9
|
+
* concept of "before THIS campaign"). Returns an empty list for a campaign
|
|
10
|
+
* that is not halted — there is nothing to propose while it is still
|
|
11
|
+
* running or after it fully succeeded.
|
|
12
|
+
*/
|
|
13
|
+
export async function listRollbackCandidates(campaignStore, campaign) {
|
|
14
|
+
if (campaign.status !== 'halted')
|
|
15
|
+
return [];
|
|
16
|
+
const candidates = [];
|
|
17
|
+
for (const siteId of campaign.failedSiteIds) {
|
|
18
|
+
const records = await campaignStore.getSiteRolloutRecords(campaign.id, siteId);
|
|
19
|
+
// A site appears at most once per campaign in practice (task 7's wave
|
|
20
|
+
// partition is disjoint) — the failed/most recent record is the real
|
|
21
|
+
// one to read "what it reported right before this campaign" from.
|
|
22
|
+
const record = records.at(-1);
|
|
23
|
+
candidates.push({
|
|
24
|
+
siteId,
|
|
25
|
+
componentKind: campaign.componentKind,
|
|
26
|
+
componentName: campaign.componentName,
|
|
27
|
+
rollbackToVersion: record?.preUpdateVersion ?? null,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return candidates;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Enqueues a real, signed `rollback` command (task 6's real command-queue
|
|
34
|
+
* primitive) for exactly one site — strictly per-site, the same isolation
|
|
35
|
+
* discipline every prior L8 task has held: there is no "roll back the whole
|
|
36
|
+
* fleet" operation, matching "## Mises à jour groupées"'s own words
|
|
37
|
+
* verbatim: "Chaque site conserve son propre historique de version et peut
|
|
38
|
+
* être revenu en arrière indépendamment. Il n'existe pas d'état global à
|
|
39
|
+
* restaurer." Callable standalone, independent of any campaign — an
|
|
40
|
+
* operator deciding a specific site needs to go back does not need a
|
|
41
|
+
* campaign to exist at all; `listRollbackCandidates` above is one real way
|
|
42
|
+
* to arrive at the right `(siteId, componentKind, componentName, version)`
|
|
43
|
+
* tuple, not the only way to call this.
|
|
44
|
+
*/
|
|
45
|
+
export async function triggerRollback(commandQueueStore, siteId, componentKind, componentName, rollbackToVersion) {
|
|
46
|
+
if (rollbackToVersion === null) {
|
|
47
|
+
throw new CogentaError({
|
|
48
|
+
code: 'FLEET_ROLLBACK_NO_PRIOR_VERSION',
|
|
49
|
+
message: `No prior version is known for "${componentName}" on site "${siteId}" — nothing to roll back to.`,
|
|
50
|
+
hint: 'A rollback needs a real, previously-reported version. Check the site actually reported this component before the change that needs reverting.',
|
|
51
|
+
details: { siteId, componentKind, componentName },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return await commandQueueStore.enqueue(siteId, 'rollback', {
|
|
55
|
+
componentKind,
|
|
56
|
+
componentName,
|
|
57
|
+
targetVersion: rollbackToVersion,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=rollback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rollback.js","sourceRoot":"","sources":["../../src/rollout/rollback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AA2B5C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,aAAmC,EACnC,QAAwB;IAExB,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAA;IAE3C,MAAM,UAAU,GAAwB,EAAE,CAAA;IAC1C,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;QAC5C,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;QAC9E,sEAAsE;QACtE,qEAAqE;QACrE,kEAAkE;QAClE,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;QAC7B,UAAU,CAAC,IAAI,CAAC;YACd,MAAM;YACN,aAAa,EAAE,QAAQ,CAAC,aAAa;YACrC,aAAa,EAAE,QAAQ,CAAC,aAAa;YACrC,iBAAiB,EAAE,MAAM,EAAE,gBAAgB,IAAI,IAAI;SACpD,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,UAAU,CAAA;AACnB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,iBAAoC,EACpC,MAAc,EACd,aAAqC,EACrC,aAAqB,EACrB,iBAAgC;IAEhC,IAAI,iBAAiB,KAAK,IAAI,EAAE,CAAC;QAC/B,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,iCAAiC;YACvC,OAAO,EAAE,kCAAkC,aAAa,cAAc,MAAM,8BAA8B;YAC1G,IAAI,EAAE,+IAA+I;YACrJ,OAAO,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE;SAClD,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,MAAM,iBAAiB,CAAC,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE;QACzD,aAAa;QACb,aAAa;QACb,aAAa,EAAE,iBAAiB;KACjC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
export declare const ROLLOUT_TABLES: {
|
|
3
|
+
readonly campaigns: 'cogenta_fleet_rollout_campaigns';
|
|
4
|
+
readonly siteStatus: 'cogenta_fleet_rollout_site_status';
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Same `create table if not exists` pattern as `../control/tables.ts` — no
|
|
8
|
+
* separate migration file. `campaigns` is one row per rollout campaign, its
|
|
9
|
+
* whole wave plan persisted as JSON so a campaign genuinely survives a
|
|
10
|
+
* control-plane restart ("le plan de contrôle indisponible n'affecte aucun
|
|
11
|
+
* site" cuts both ways: an in-flight campaign's own state must be real,
|
|
12
|
+
* durable data, never in-memory only). `site_status` is one row per
|
|
13
|
+
* (campaign, site) — the real per-site version-history record a rollback
|
|
14
|
+
* (task 8) needs: `pre_update_version` is what to roll back TO, not a
|
|
15
|
+
* duplicate of `../control/state.ts`'s general telemetry history, which
|
|
16
|
+
* tracks drift over time rather than "what this one campaign changed."
|
|
17
|
+
*/
|
|
18
|
+
export declare function ensureRolloutTables(db: DatabaseHandle): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=tables.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/rollout/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAEtB,eAAO,MAAM,cAAc;aACzB,SAAS,EAAE,iCAAiC;aAC5C,UAAU,EAAE,mCAAmC;CACvC,CAAA;AA0BV;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC3E"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { identifier, sql, unsafeRaw, } from '@cogenta/core';
|
|
2
|
+
export const ROLLOUT_TABLES = {
|
|
3
|
+
campaigns: 'cogenta_fleet_rollout_campaigns',
|
|
4
|
+
siteStatus: 'cogenta_fleet_rollout_site_status',
|
|
5
|
+
};
|
|
6
|
+
function textColumn(dialect, length) {
|
|
7
|
+
return unsafeRaw(dialect === 'sqlite' ? 'text' : `varchar(${length})`);
|
|
8
|
+
}
|
|
9
|
+
function longTextColumn(dialect) {
|
|
10
|
+
return unsafeRaw(dialect === 'sqlite' ? 'text' : 'text');
|
|
11
|
+
}
|
|
12
|
+
/** A real integer column — `current_wave_index`/`wave_index` are ordinal positions compared and incremented as numbers, never text. */
|
|
13
|
+
function intColumn() {
|
|
14
|
+
return unsafeRaw('integer');
|
|
15
|
+
}
|
|
16
|
+
async function createIndexIfMissing(db, name, table, columns) {
|
|
17
|
+
await db
|
|
18
|
+
.query(sql `create index ${identifier(name, db.dialect)} on ${table} ${columns}`)
|
|
19
|
+
.catch(() => undefined); // already there — no portable "if not exists" for indexes
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Same `create table if not exists` pattern as `../control/tables.ts` — no
|
|
23
|
+
* separate migration file. `campaigns` is one row per rollout campaign, its
|
|
24
|
+
* whole wave plan persisted as JSON so a campaign genuinely survives a
|
|
25
|
+
* control-plane restart ("le plan de contrôle indisponible n'affecte aucun
|
|
26
|
+
* site" cuts both ways: an in-flight campaign's own state must be real,
|
|
27
|
+
* durable data, never in-memory only). `site_status` is one row per
|
|
28
|
+
* (campaign, site) — the real per-site version-history record a rollback
|
|
29
|
+
* (task 8) needs: `pre_update_version` is what to roll back TO, not a
|
|
30
|
+
* duplicate of `../control/state.ts`'s general telemetry history, which
|
|
31
|
+
* tracks drift over time rather than "what this one campaign changed."
|
|
32
|
+
*/
|
|
33
|
+
export async function ensureRolloutTables(db) {
|
|
34
|
+
const d = db.dialect;
|
|
35
|
+
const t255 = textColumn(d, 255);
|
|
36
|
+
const longText = longTextColumn(d);
|
|
37
|
+
const int = intColumn();
|
|
38
|
+
const campaigns = identifier(ROLLOUT_TABLES.campaigns, d);
|
|
39
|
+
await db.query(sql `
|
|
40
|
+
create table if not exists ${campaigns} (
|
|
41
|
+
id ${t255} not null primary key,
|
|
42
|
+
component_kind ${t255} not null,
|
|
43
|
+
component_name ${t255} not null,
|
|
44
|
+
target_version ${t255} not null,
|
|
45
|
+
waves_json ${longText} not null,
|
|
46
|
+
current_wave_index ${int} not null,
|
|
47
|
+
status ${t255} not null,
|
|
48
|
+
halted_reason ${longText},
|
|
49
|
+
created_at ${t255} not null
|
|
50
|
+
)`);
|
|
51
|
+
const siteStatus = identifier(ROLLOUT_TABLES.siteStatus, d);
|
|
52
|
+
await db.query(sql `
|
|
53
|
+
create table if not exists ${siteStatus} (
|
|
54
|
+
id ${t255} not null primary key,
|
|
55
|
+
campaign_id ${t255} not null,
|
|
56
|
+
site_id ${t255} not null,
|
|
57
|
+
wave_index ${int} not null,
|
|
58
|
+
status ${t255} not null,
|
|
59
|
+
pre_update_version ${t255},
|
|
60
|
+
dispatched_at ${t255},
|
|
61
|
+
resolved_at ${t255}
|
|
62
|
+
)`);
|
|
63
|
+
await createIndexIfMissing(db, 'cogenta_fleet_rollout_site_status_campaign', siteStatus, sql `(campaign_id, site_id)`);
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=tables.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tables.js","sourceRoot":"","sources":["../../src/rollout/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEV,GAAG,EACH,SAAS,GACV,MAAM,eAAe,CAAA;AAEtB,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,SAAS,EAAE,iCAAiC;IAC5C,UAAU,EAAE,mCAAmC;CACvC,CAAA;AAEV,SAAS,UAAU,CAAC,OAAwB,EAAE,MAAc;IAC1D,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,MAAM,GAAG,CAAC,CAAA;AACxE,CAAC;AAED,SAAS,cAAc,CAAC,OAAwB;IAC9C,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AAC1D,CAAC;AAED,uIAAuI;AACvI,SAAS,SAAS;IAChB,OAAO,SAAS,CAAC,SAAS,CAAC,CAAA;AAC7B,CAAC;AAED,KAAK,UAAU,oBAAoB,CACjC,EAAkB,EAClB,IAAY,EACZ,KAAkB,EAClB,OAAoB;IAEpB,MAAM,EAAE;SACL,KAAK,CAAC,GAAG,CAAA,gBAAgB,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,KAAK,IAAI,OAAO,EAAE,CAAC;SAC/E,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA,CAAC,0DAA0D;AACtF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAkB;IAC1D,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAA;IACpB,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC/B,MAAM,QAAQ,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA;IAClC,MAAM,GAAG,GAAG,SAAS,EAAE,CAAA;IAEvB,MAAM,SAAS,GAAG,UAAU,CAAC,cAAc,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;IACzD,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,SAAS;WAC/B,IAAI;uBACQ,IAAI;uBACJ,IAAI;uBACJ,IAAI;mBACR,QAAQ;2BACA,GAAG;eACf,IAAI;sBACG,QAAQ;mBACX,IAAI;MACjB,CAAC,CAAA;IAEL,MAAM,UAAU,GAAG,UAAU,CAAC,cAAc,CAAC,UAAU,EAAE,CAAC,CAAC,CAAA;IAC3D,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU;WAChC,IAAI;oBACK,IAAI;gBACR,IAAI;mBACD,GAAG;eACP,IAAI;2BACQ,IAAI;sBACT,IAAI;oBACN,IAAI;MAClB,CAAC,CAAA;IACL,MAAM,oBAAoB,CACxB,EAAE,EACF,4CAA4C,EAC5C,UAAU,EACV,GAAG,CAAA,wBAAwB,CAC5B,CAAA;AACH,CAAC"}
|