@dsh-enhanced/plugin-control-plane 0.1.6 → 0.1.12
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/README.md +153 -32
- package/bin/dsh-local-release-adapter.js +1381 -0
- package/cordis.patch.yml +1 -0
- package/lib/approval.d.ts +13 -0
- package/lib/approval.d.ts.map +1 -0
- package/lib/approval.js +79 -0
- package/lib/approval.js.map +1 -0
- package/lib/attestation.d.ts +14 -0
- package/lib/attestation.d.ts.map +1 -0
- package/lib/attestation.js +222 -0
- package/lib/attestation.js.map +1 -0
- package/lib/catalog-interpreter.d.ts +12 -0
- package/lib/catalog-interpreter.d.ts.map +1 -0
- package/lib/catalog-interpreter.js +100 -0
- package/lib/catalog-interpreter.js.map +1 -0
- package/lib/catalog.d.ts +95 -10
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +1031 -18
- package/lib/catalog.js.map +1 -1
- package/lib/cli.d.ts +9 -0
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +1160 -162
- package/lib/cli.js.map +1 -1
- package/lib/host-attestor.d.ts +13 -0
- package/lib/host-attestor.d.ts.map +1 -0
- package/lib/host-attestor.js +139 -0
- package/lib/host-attestor.js.map +1 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +9 -0
- package/lib/index.js.map +1 -1
- package/lib/lockfile.d.ts +7 -0
- package/lib/lockfile.d.ts.map +1 -0
- package/lib/lockfile.js +271 -0
- package/lib/lockfile.js.map +1 -0
- package/lib/release.d.ts +51 -0
- package/lib/release.d.ts.map +1 -0
- package/lib/release.js +1188 -0
- package/lib/release.js.map +1 -0
- package/lib/service.d.ts +9 -11
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +57 -29
- package/lib/service.js.map +1 -1
- package/lib/sqlite.d.ts +9 -0
- package/lib/sqlite.d.ts.map +1 -0
- package/lib/sqlite.js +705 -0
- package/lib/sqlite.js.map +1 -0
- package/lib/store.d.ts +258 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/store.js +1848 -0
- package/lib/store.js.map +1 -0
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +23 -3
- package/lib/tools.js.map +1 -1
- package/lib/trust.d.ts +79 -0
- package/lib/trust.d.ts.map +1 -0
- package/lib/trust.js +477 -0
- package/lib/trust.js.map +1 -0
- package/lib/types.d.ts +740 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.d.ts.map +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/package.json +2 -2
package/lib/sqlite.js
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
export const controlPlaneSchemaVersion = 11;
|
|
6
|
+
export function controlPlaneOperationReceiptDigest(idempotencyKey, operation, inputDigest, resultJson, createdAt) {
|
|
7
|
+
return createHash('sha256').update(JSON.stringify([idempotencyKey, operation, inputDigest, resultJson, createdAt])).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
export class ControlPlaneDatabaseError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = 'ControlPlaneDatabaseError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function assertPrivateRegularFile(path) {
|
|
18
|
+
const value = lstatSync(path);
|
|
19
|
+
const uid = process.getuid?.();
|
|
20
|
+
if (!value.isFile() || value.isSymbolicLink() || value.nlink !== 1
|
|
21
|
+
|| (value.mode & 0o077) !== 0 || (uid !== undefined && value.uid !== uid)) {
|
|
22
|
+
throw new ControlPlaneDatabaseError('unsafe-file', 'plugin-control-plane database must be a private, owner-owned regular file');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function prepare(path) {
|
|
26
|
+
if (!isAbsolute(path))
|
|
27
|
+
throw new ControlPlaneDatabaseError('invalid-path', 'plugin-control-plane database path must be absolute');
|
|
28
|
+
const directory = dirname(path);
|
|
29
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
30
|
+
const directoryStat = lstatSync(directory);
|
|
31
|
+
const uid = process.getuid?.();
|
|
32
|
+
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink() || realpathSync(directory) !== resolve(directory)
|
|
33
|
+
|| (directoryStat.mode & 0o077) !== 0 || (uid !== undefined && directoryStat.uid !== uid)) {
|
|
34
|
+
throw new ControlPlaneDatabaseError('unsafe-file', 'plugin-control-plane database directory must be private and must not traverse symlinks');
|
|
35
|
+
}
|
|
36
|
+
if (!existsSync(path))
|
|
37
|
+
closeSync(openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR, 0o600));
|
|
38
|
+
assertPrivateRegularFile(path);
|
|
39
|
+
for (const sidecar of [`${path}-wal`, `${path}-shm`])
|
|
40
|
+
if (existsSync(sidecar))
|
|
41
|
+
assertPrivateRegularFile(sidecar);
|
|
42
|
+
}
|
|
43
|
+
function createCurrent(database) {
|
|
44
|
+
database.exec(`
|
|
45
|
+
CREATE TABLE capability_gaps (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
48
|
+
input_digest TEXT NOT NULL CHECK(length(input_digest) = 64),
|
|
49
|
+
capability TEXT NOT NULL,
|
|
50
|
+
context TEXT NOT NULL,
|
|
51
|
+
expected_value REAL NOT NULL CHECK(expected_value >= 0),
|
|
52
|
+
frequency REAL NOT NULL CHECK(frequency > 0),
|
|
53
|
+
estimated_cost REAL NOT NULL CHECK(estimated_cost > 0),
|
|
54
|
+
risk REAL NOT NULL CHECK(risk >= 0 AND risk <= 1),
|
|
55
|
+
roi REAL NOT NULL,
|
|
56
|
+
status TEXT NOT NULL CHECK(status IN ('open', 'matched', 'closed')),
|
|
57
|
+
candidate_id TEXT,
|
|
58
|
+
revision INTEGER NOT NULL CHECK(revision >= 1),
|
|
59
|
+
created_at INTEGER NOT NULL,
|
|
60
|
+
updated_at INTEGER NOT NULL
|
|
61
|
+
) STRICT;
|
|
62
|
+
|
|
63
|
+
CREATE TABLE activation_plans (
|
|
64
|
+
id TEXT PRIMARY KEY,
|
|
65
|
+
plan_digest TEXT NOT NULL UNIQUE CHECK(length(plan_digest) = 64),
|
|
66
|
+
gap_id TEXT NOT NULL,
|
|
67
|
+
gap_snapshot_json TEXT NOT NULL CHECK(json_valid(gap_snapshot_json) AND json_type(gap_snapshot_json) = 'object'),
|
|
68
|
+
profile TEXT NOT NULL,
|
|
69
|
+
candidate_json TEXT NOT NULL CHECK(json_valid(candidate_json) AND json_type(candidate_json) = 'object'),
|
|
70
|
+
dossier_json TEXT NOT NULL CHECK(json_valid(dossier_json) AND json_type(dossier_json) = 'object'),
|
|
71
|
+
installation_id TEXT NOT NULL,
|
|
72
|
+
ledger_id TEXT NOT NULL,
|
|
73
|
+
ledger_path TEXT NOT NULL,
|
|
74
|
+
dsh_home TEXT NOT NULL,
|
|
75
|
+
target_path TEXT NOT NULL,
|
|
76
|
+
executor_id TEXT NOT NULL,
|
|
77
|
+
executor_version TEXT NOT NULL,
|
|
78
|
+
executor_path TEXT NOT NULL,
|
|
79
|
+
executor_digest TEXT NOT NULL CHECK(length(executor_digest) = 64),
|
|
80
|
+
status TEXT NOT NULL CHECK(status IN (
|
|
81
|
+
'pending-approval', 'approved', 'staging', 'awaiting-reload',
|
|
82
|
+
'awaiting-readiness', 'awaiting-effect-blocked-replay', 'awaiting-shadow',
|
|
83
|
+
'awaiting-canary', 'awaiting-soak', 'awaiting-health',
|
|
84
|
+
'commit-pending', 'rollback-pending', 'activated', 'rolled-back'
|
|
85
|
+
)),
|
|
86
|
+
revision INTEGER NOT NULL CHECK(revision >= 1),
|
|
87
|
+
created_at INTEGER NOT NULL,
|
|
88
|
+
expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
|
|
89
|
+
approval_json TEXT CHECK(approval_json IS NULL OR json_valid(approval_json)),
|
|
90
|
+
approval_receipt_json TEXT CHECK(approval_receipt_json IS NULL OR (json_valid(approval_receipt_json) AND json_type(approval_receipt_json) = 'object')),
|
|
91
|
+
activation_id TEXT,
|
|
92
|
+
activation_fence INTEGER NOT NULL DEFAULT 0 CHECK(activation_fence >= 0),
|
|
93
|
+
activation_lease_until INTEGER,
|
|
94
|
+
activation_target_existed INTEGER CHECK(activation_target_existed IN (0, 1)),
|
|
95
|
+
failure_code TEXT,
|
|
96
|
+
updated_at INTEGER NOT NULL,
|
|
97
|
+
FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
|
|
98
|
+
) STRICT;
|
|
99
|
+
|
|
100
|
+
CREATE INDEX activation_plans_status ON activation_plans(status, updated_at, id);
|
|
101
|
+
CREATE UNIQUE INDEX activation_plans_active_target ON activation_plans(target_path) WHERE status IN (
|
|
102
|
+
'staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-effect-blocked-replay',
|
|
103
|
+
'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health',
|
|
104
|
+
'commit-pending', 'rollback-pending'
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
CREATE TABLE source_plans (
|
|
108
|
+
id TEXT PRIMARY KEY,
|
|
109
|
+
plan_digest TEXT NOT NULL UNIQUE CHECK(length(plan_digest) = 64),
|
|
110
|
+
gap_id TEXT NOT NULL,
|
|
111
|
+
gap_snapshot_json TEXT NOT NULL CHECK(json_valid(gap_snapshot_json) AND json_type(gap_snapshot_json) = 'object'),
|
|
112
|
+
repository TEXT NOT NULL,
|
|
113
|
+
worktree TEXT NOT NULL,
|
|
114
|
+
base_commit TEXT NOT NULL CHECK(length(base_commit) = 40),
|
|
115
|
+
plugin_name TEXT NOT NULL,
|
|
116
|
+
generator_digest TEXT NOT NULL CHECK(length(generator_digest) = 64),
|
|
117
|
+
scope_json TEXT NOT NULL CHECK(json_valid(scope_json) AND json_type(scope_json) = 'array'),
|
|
118
|
+
status TEXT NOT NULL CHECK(status IN (
|
|
119
|
+
'pending-approval', 'approved', 'running-local-checks', 'ready-for-human-review', 'local-checks-failed',
|
|
120
|
+
'awaiting-pr', 'awaiting-review', 'awaiting-merge', 'awaiting-build', 'awaiting-sign', 'awaiting-publish',
|
|
121
|
+
'awaiting-registry-verify', 'awaiting-catalog-admission', 'release-complete', 'release-failed', 'publish-ambiguous'
|
|
122
|
+
)),
|
|
123
|
+
revision INTEGER NOT NULL CHECK(revision >= 1),
|
|
124
|
+
created_at INTEGER NOT NULL,
|
|
125
|
+
expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
|
|
126
|
+
approval_json TEXT CHECK(approval_json IS NULL OR json_valid(approval_json)),
|
|
127
|
+
checked_tree_digest TEXT CHECK(checked_tree_digest IS NULL OR (length(checked_tree_digest) = 64 AND checked_tree_digest NOT GLOB '*[^a-f0-9]*')),
|
|
128
|
+
checked_patch_digest TEXT CHECK(checked_patch_digest IS NULL OR (length(checked_patch_digest) = 64 AND checked_patch_digest NOT GLOB '*[^a-f0-9]*')),
|
|
129
|
+
checked_at INTEGER,
|
|
130
|
+
release_authorization_json TEXT CHECK(release_authorization_json IS NULL OR (json_valid(release_authorization_json) AND json_type(release_authorization_json) = 'object')),
|
|
131
|
+
release_authorization_digest TEXT CHECK(release_authorization_digest IS NULL OR length(release_authorization_digest) = 64),
|
|
132
|
+
release_id TEXT,
|
|
133
|
+
release_fence INTEGER NOT NULL DEFAULT 0 CHECK(release_fence >= 0),
|
|
134
|
+
release_failure_phase TEXT CHECK(release_failure_phase IS NULL OR release_failure_phase IN (
|
|
135
|
+
'pr', 'review', 'merge', 'build', 'sign', 'publish', 'registry-verify', 'catalog-admission'
|
|
136
|
+
)),
|
|
137
|
+
release_failure_code TEXT,
|
|
138
|
+
updated_at INTEGER NOT NULL,
|
|
139
|
+
CHECK((checked_tree_digest IS NULL AND checked_patch_digest IS NULL AND checked_at IS NULL) OR
|
|
140
|
+
(checked_tree_digest IS NOT NULL AND checked_patch_digest IS NOT NULL AND checked_at IS NOT NULL)),
|
|
141
|
+
CHECK((release_authorization_json IS NULL AND release_authorization_digest IS NULL) OR
|
|
142
|
+
(release_authorization_json IS NOT NULL AND release_authorization_digest IS NOT NULL)),
|
|
143
|
+
FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
|
|
144
|
+
) STRICT;
|
|
145
|
+
|
|
146
|
+
CREATE TABLE gap_plan_claims (
|
|
147
|
+
gap_id TEXT PRIMARY KEY,
|
|
148
|
+
plan_id TEXT NOT NULL UNIQUE,
|
|
149
|
+
plan_kind TEXT NOT NULL CHECK(plan_kind IN ('activation', 'source')),
|
|
150
|
+
claimed_at INTEGER NOT NULL,
|
|
151
|
+
FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
|
|
152
|
+
) STRICT, WITHOUT ROWID;
|
|
153
|
+
|
|
154
|
+
CREATE TABLE host_attestations (
|
|
155
|
+
plan_id TEXT NOT NULL,
|
|
156
|
+
phase TEXT NOT NULL CHECK(phase IN ('reload', 'readiness', 'effect-blocked-replay', 'shadow', 'canary', 'soak', 'health')),
|
|
157
|
+
receipt_id TEXT NOT NULL UNIQUE,
|
|
158
|
+
receipt_digest TEXT NOT NULL CHECK(length(receipt_digest) = 64),
|
|
159
|
+
receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json) AND json_type(receipt_json) = 'object'),
|
|
160
|
+
host_generation INTEGER NOT NULL CHECK(host_generation >= 1),
|
|
161
|
+
created_at INTEGER NOT NULL,
|
|
162
|
+
PRIMARY KEY(plan_id, phase),
|
|
163
|
+
FOREIGN KEY(plan_id) REFERENCES activation_plans(id) ON DELETE RESTRICT
|
|
164
|
+
) STRICT, WITHOUT ROWID;
|
|
165
|
+
|
|
166
|
+
CREATE TABLE host_attestation_operations (
|
|
167
|
+
plan_id TEXT NOT NULL,
|
|
168
|
+
phase TEXT NOT NULL CHECK(phase IN ('reload', 'readiness', 'effect-blocked-replay', 'shadow', 'canary', 'soak', 'health')),
|
|
169
|
+
operation_id TEXT NOT NULL UNIQUE,
|
|
170
|
+
binding_digest TEXT NOT NULL CHECK(length(binding_digest) = 64),
|
|
171
|
+
request_digest TEXT NOT NULL CHECK(length(request_digest) = 64),
|
|
172
|
+
request_json TEXT NOT NULL CHECK(json_valid(request_json) AND json_type(request_json) = 'object'),
|
|
173
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'completed', 'applied')),
|
|
174
|
+
receipt_digest TEXT CHECK(receipt_digest IS NULL OR length(receipt_digest) = 64),
|
|
175
|
+
receipt_json TEXT CHECK(receipt_json IS NULL OR (json_valid(receipt_json) AND json_type(receipt_json) = 'object')),
|
|
176
|
+
created_at INTEGER NOT NULL,
|
|
177
|
+
completed_at INTEGER,
|
|
178
|
+
applied_at INTEGER,
|
|
179
|
+
PRIMARY KEY(plan_id, phase),
|
|
180
|
+
FOREIGN KEY(plan_id) REFERENCES activation_plans(id) ON DELETE RESTRICT
|
|
181
|
+
) STRICT, WITHOUT ROWID;
|
|
182
|
+
|
|
183
|
+
CREATE TABLE source_release_operations (
|
|
184
|
+
plan_id TEXT NOT NULL,
|
|
185
|
+
phase TEXT NOT NULL CHECK(phase IN ('pr', 'review', 'merge', 'build', 'sign', 'publish', 'registry-verify', 'catalog-admission')),
|
|
186
|
+
release_id TEXT NOT NULL,
|
|
187
|
+
release_fence INTEGER NOT NULL CHECK(release_fence >= 1),
|
|
188
|
+
attempt INTEGER NOT NULL CHECK(attempt >= 1),
|
|
189
|
+
operation_id TEXT NOT NULL UNIQUE,
|
|
190
|
+
binding_digest TEXT NOT NULL CHECK(length(binding_digest) = 64),
|
|
191
|
+
request_digest TEXT NOT NULL CHECK(length(request_digest) = 64),
|
|
192
|
+
request_json TEXT NOT NULL CHECK(json_valid(request_json) AND json_type(request_json) = 'object'),
|
|
193
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'completed', 'applied')),
|
|
194
|
+
receipt_digest TEXT CHECK(receipt_digest IS NULL OR length(receipt_digest) = 64),
|
|
195
|
+
receipt_json TEXT CHECK(receipt_json IS NULL OR (json_valid(receipt_json) AND json_type(receipt_json) = 'object')),
|
|
196
|
+
created_at INTEGER NOT NULL,
|
|
197
|
+
completed_at INTEGER,
|
|
198
|
+
applied_at INTEGER,
|
|
199
|
+
PRIMARY KEY(plan_id, phase, attempt),
|
|
200
|
+
UNIQUE(plan_id, phase, release_fence),
|
|
201
|
+
CHECK(
|
|
202
|
+
(status = 'pending' AND receipt_digest IS NULL AND receipt_json IS NULL AND completed_at IS NULL AND applied_at IS NULL) OR
|
|
203
|
+
(status = 'completed' AND receipt_digest IS NOT NULL AND receipt_json IS NOT NULL AND completed_at IS NOT NULL AND applied_at IS NULL) OR
|
|
204
|
+
(status = 'applied' AND receipt_digest IS NOT NULL AND receipt_json IS NOT NULL AND completed_at IS NOT NULL AND applied_at IS NOT NULL)
|
|
205
|
+
),
|
|
206
|
+
FOREIGN KEY(plan_id) REFERENCES source_plans(id) ON DELETE RESTRICT
|
|
207
|
+
) STRICT, WITHOUT ROWID;
|
|
208
|
+
|
|
209
|
+
CREATE TABLE operation_receipts (
|
|
210
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
211
|
+
operation TEXT NOT NULL,
|
|
212
|
+
input_digest TEXT NOT NULL CHECK(length(input_digest) = 64),
|
|
213
|
+
result_json TEXT NOT NULL CHECK(json_valid(result_json)),
|
|
214
|
+
result_digest TEXT NOT NULL CHECK(length(result_digest) = 64),
|
|
215
|
+
created_at INTEGER NOT NULL
|
|
216
|
+
) STRICT, WITHOUT ROWID;
|
|
217
|
+
|
|
218
|
+
CREATE TABLE quarantined_legacy_plans (
|
|
219
|
+
source TEXT PRIMARY KEY,
|
|
220
|
+
reason TEXT NOT NULL,
|
|
221
|
+
payload_json TEXT NOT NULL,
|
|
222
|
+
quarantined_at INTEGER NOT NULL
|
|
223
|
+
) STRICT, WITHOUT ROWID;
|
|
224
|
+
|
|
225
|
+
CREATE TABLE source_publish_reconciliations (
|
|
226
|
+
plan_id TEXT NOT NULL,
|
|
227
|
+
release_id TEXT NOT NULL,
|
|
228
|
+
release_fence INTEGER NOT NULL CHECK(release_fence >= 1),
|
|
229
|
+
attempt INTEGER NOT NULL CHECK(attempt >= 1),
|
|
230
|
+
operation_id TEXT NOT NULL,
|
|
231
|
+
binding_digest TEXT NOT NULL CHECK(length(binding_digest) = 64),
|
|
232
|
+
request_digest TEXT NOT NULL CHECK(length(request_digest) = 64),
|
|
233
|
+
request_json TEXT NOT NULL CHECK(json_valid(request_json) AND json_type(request_json) = 'object'),
|
|
234
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'completed', 'applied')),
|
|
235
|
+
receipt_digest TEXT CHECK(receipt_digest IS NULL OR length(receipt_digest) = 64),
|
|
236
|
+
receipt_json TEXT CHECK(receipt_json IS NULL OR (json_valid(receipt_json) AND json_type(receipt_json) = 'object')),
|
|
237
|
+
created_at INTEGER NOT NULL,
|
|
238
|
+
completed_at INTEGER,
|
|
239
|
+
applied_at INTEGER,
|
|
240
|
+
PRIMARY KEY(plan_id, attempt),
|
|
241
|
+
UNIQUE(operation_id),
|
|
242
|
+
CHECK(
|
|
243
|
+
(status = 'pending' AND receipt_digest IS NULL AND receipt_json IS NULL AND completed_at IS NULL AND applied_at IS NULL) OR
|
|
244
|
+
(status = 'completed' AND receipt_digest IS NOT NULL AND receipt_json IS NOT NULL AND completed_at IS NOT NULL AND applied_at IS NULL) OR
|
|
245
|
+
(status = 'applied' AND receipt_digest IS NOT NULL AND receipt_json IS NOT NULL AND completed_at IS NOT NULL AND applied_at IS NOT NULL)
|
|
246
|
+
),
|
|
247
|
+
FOREIGN KEY(plan_id) REFERENCES source_plans(id) ON DELETE RESTRICT
|
|
248
|
+
) STRICT, WITHOUT ROWID;
|
|
249
|
+
CREATE INDEX source_publish_reconciliations_release ON source_publish_reconciliations(plan_id, release_fence, created_at);
|
|
250
|
+
|
|
251
|
+
PRAGMA user_version = 11;
|
|
252
|
+
`);
|
|
253
|
+
}
|
|
254
|
+
function migrateV1(database) {
|
|
255
|
+
// v1 used mutable JSON plan files and carried no authoritative digest-bound
|
|
256
|
+
// approval. Preserve them only as evidence; none can become activation input.
|
|
257
|
+
const tables = database.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all();
|
|
258
|
+
const evidence = [];
|
|
259
|
+
for (const { name } of tables) {
|
|
260
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name))
|
|
261
|
+
throw new ControlPlaneDatabaseError('unsafe-file', 'legacy database contains an unsafe table name');
|
|
262
|
+
const rows = database.prepare(`SELECT * FROM "${name}"`).all();
|
|
263
|
+
rows.forEach((row, index) => evidence.push({ source: `${name}:${index}`, payload: JSON.stringify(row) }));
|
|
264
|
+
}
|
|
265
|
+
database.exec('BEGIN IMMEDIATE');
|
|
266
|
+
try {
|
|
267
|
+
for (const { name } of tables)
|
|
268
|
+
database.exec(`DROP TABLE "${name}"`);
|
|
269
|
+
createCurrent(database);
|
|
270
|
+
const insert = database.prepare('INSERT INTO quarantined_legacy_plans (source, reason, payload_json, quarantined_at) VALUES (?, ?, ?, ?)');
|
|
271
|
+
for (const item of evidence)
|
|
272
|
+
insert.run(item.source, 'legacy-json-approval-is-not-authoritative', item.payload, Date.now());
|
|
273
|
+
database.exec('COMMIT');
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
database.exec('ROLLBACK');
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function migrateV2ToV3(database) {
|
|
281
|
+
database.exec(`
|
|
282
|
+
PRAGMA foreign_keys = OFF;
|
|
283
|
+
BEGIN IMMEDIATE;
|
|
284
|
+
ALTER TABLE activation_plans RENAME TO activation_plans_v2;
|
|
285
|
+
CREATE TABLE activation_plans (
|
|
286
|
+
id TEXT PRIMARY KEY,
|
|
287
|
+
plan_digest TEXT NOT NULL UNIQUE CHECK(length(plan_digest) = 64),
|
|
288
|
+
gap_id TEXT,
|
|
289
|
+
profile TEXT NOT NULL,
|
|
290
|
+
candidate_json TEXT NOT NULL CHECK(json_valid(candidate_json) AND json_type(candidate_json) = 'object'),
|
|
291
|
+
status TEXT NOT NULL CHECK(status IN (
|
|
292
|
+
'pending-approval', 'approved', 'staging', 'readiness',
|
|
293
|
+
'effect-blocked-replay', 'soaking', 'rollback-pending', 'activated',
|
|
294
|
+
'rolled-back', 'ready-for-human-review'
|
|
295
|
+
)),
|
|
296
|
+
revision INTEGER NOT NULL CHECK(revision >= 1),
|
|
297
|
+
created_at INTEGER NOT NULL,
|
|
298
|
+
expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
|
|
299
|
+
approval_json TEXT CHECK(approval_json IS NULL OR json_valid(approval_json)),
|
|
300
|
+
activation_id TEXT,
|
|
301
|
+
activation_fence INTEGER NOT NULL DEFAULT 0 CHECK(activation_fence >= 0),
|
|
302
|
+
activation_lease_until INTEGER,
|
|
303
|
+
failure_code TEXT,
|
|
304
|
+
updated_at INTEGER NOT NULL,
|
|
305
|
+
FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
|
|
306
|
+
) STRICT;
|
|
307
|
+
INSERT INTO activation_plans SELECT * FROM activation_plans_v2;
|
|
308
|
+
DROP TABLE activation_plans_v2;
|
|
309
|
+
CREATE INDEX activation_plans_status ON activation_plans(status, updated_at, id);
|
|
310
|
+
PRAGMA user_version = 3;
|
|
311
|
+
COMMIT;
|
|
312
|
+
PRAGMA foreign_keys = ON;
|
|
313
|
+
`);
|
|
314
|
+
}
|
|
315
|
+
function migrateV3ToV4(database) {
|
|
316
|
+
const rows = database.prepare('SELECT * FROM activation_plans').all();
|
|
317
|
+
database.exec('BEGIN IMMEDIATE');
|
|
318
|
+
try {
|
|
319
|
+
const quarantine = database.prepare('INSERT OR REPLACE INTO quarantined_legacy_plans (source, reason, payload_json, quarantined_at) VALUES (?, ?, ?, ?)');
|
|
320
|
+
rows.forEach((row, index) => quarantine.run(`activation_plans_v3:${index}`, 'activation-plan-lacks-installation-target-executor-and-host-attestation-binding', JSON.stringify(row), Date.now()));
|
|
321
|
+
database.exec(`
|
|
322
|
+
DROP INDEX IF EXISTS activation_plans_status;
|
|
323
|
+
DROP TABLE activation_plans;
|
|
324
|
+
CREATE TABLE activation_plans (
|
|
325
|
+
id TEXT PRIMARY KEY,
|
|
326
|
+
plan_digest TEXT NOT NULL UNIQUE CHECK(length(plan_digest) = 64),
|
|
327
|
+
gap_id TEXT NOT NULL,
|
|
328
|
+
gap_snapshot_json TEXT NOT NULL CHECK(json_valid(gap_snapshot_json) AND json_type(gap_snapshot_json) = 'object'),
|
|
329
|
+
profile TEXT NOT NULL,
|
|
330
|
+
candidate_json TEXT NOT NULL CHECK(json_valid(candidate_json) AND json_type(candidate_json) = 'object'),
|
|
331
|
+
dossier_json TEXT NOT NULL CHECK(json_valid(dossier_json) AND json_type(dossier_json) = 'object'),
|
|
332
|
+
installation_id TEXT NOT NULL,
|
|
333
|
+
dsh_home TEXT NOT NULL,
|
|
334
|
+
target_path TEXT NOT NULL,
|
|
335
|
+
executor_id TEXT NOT NULL,
|
|
336
|
+
executor_version TEXT NOT NULL,
|
|
337
|
+
status TEXT NOT NULL CHECK(status IN (
|
|
338
|
+
'pending-approval', 'approved', 'staging', 'awaiting-reload',
|
|
339
|
+
'awaiting-readiness', 'awaiting-effect-blocked-replay', 'awaiting-shadow',
|
|
340
|
+
'awaiting-canary', 'awaiting-soak', 'awaiting-health',
|
|
341
|
+
'commit-pending', 'rollback-pending', 'activated', 'rolled-back'
|
|
342
|
+
)),
|
|
343
|
+
revision INTEGER NOT NULL CHECK(revision >= 1),
|
|
344
|
+
created_at INTEGER NOT NULL,
|
|
345
|
+
expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
|
|
346
|
+
approval_json TEXT CHECK(approval_json IS NULL OR json_valid(approval_json)),
|
|
347
|
+
activation_id TEXT,
|
|
348
|
+
activation_fence INTEGER NOT NULL DEFAULT 0 CHECK(activation_fence >= 0),
|
|
349
|
+
activation_lease_until INTEGER,
|
|
350
|
+
failure_code TEXT,
|
|
351
|
+
updated_at INTEGER NOT NULL,
|
|
352
|
+
FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
|
|
353
|
+
) STRICT;
|
|
354
|
+
CREATE INDEX activation_plans_status ON activation_plans(status, updated_at, id);
|
|
355
|
+
CREATE TABLE source_plans (
|
|
356
|
+
id TEXT PRIMARY KEY, plan_digest TEXT NOT NULL UNIQUE CHECK(length(plan_digest) = 64), gap_id TEXT NOT NULL,
|
|
357
|
+
gap_snapshot_json TEXT NOT NULL CHECK(json_valid(gap_snapshot_json) AND json_type(gap_snapshot_json) = 'object'),
|
|
358
|
+
repository TEXT NOT NULL, worktree TEXT NOT NULL, base_commit TEXT NOT NULL CHECK(length(base_commit) = 40),
|
|
359
|
+
plugin_name TEXT NOT NULL, generator_digest TEXT NOT NULL CHECK(length(generator_digest) = 64),
|
|
360
|
+
scope_json TEXT NOT NULL CHECK(json_valid(scope_json) AND json_type(scope_json) = 'array'),
|
|
361
|
+
status TEXT NOT NULL CHECK(status IN ('pending-approval', 'approved', 'running-local-checks', 'ready-for-human-review', 'local-checks-failed')),
|
|
362
|
+
revision INTEGER NOT NULL CHECK(revision >= 1), created_at INTEGER NOT NULL,
|
|
363
|
+
expires_at INTEGER NOT NULL CHECK(expires_at > created_at), approval_json TEXT CHECK(approval_json IS NULL OR json_valid(approval_json)),
|
|
364
|
+
updated_at INTEGER NOT NULL, FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
|
|
365
|
+
) STRICT;
|
|
366
|
+
CREATE TABLE gap_plan_claims (
|
|
367
|
+
gap_id TEXT PRIMARY KEY, plan_id TEXT NOT NULL UNIQUE,
|
|
368
|
+
plan_kind TEXT NOT NULL CHECK(plan_kind IN ('activation', 'source')), claimed_at INTEGER NOT NULL,
|
|
369
|
+
FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
|
|
370
|
+
) STRICT, WITHOUT ROWID;
|
|
371
|
+
CREATE TABLE host_attestations (
|
|
372
|
+
plan_id TEXT NOT NULL,
|
|
373
|
+
phase TEXT NOT NULL CHECK(phase IN ('reload', 'readiness', 'effect-blocked-replay', 'shadow', 'canary', 'soak', 'health')),
|
|
374
|
+
receipt_id TEXT NOT NULL UNIQUE, receipt_digest TEXT NOT NULL CHECK(length(receipt_digest) = 64),
|
|
375
|
+
receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json) AND json_type(receipt_json) = 'object'),
|
|
376
|
+
host_generation INTEGER NOT NULL CHECK(host_generation >= 1), created_at INTEGER NOT NULL,
|
|
377
|
+
PRIMARY KEY(plan_id, phase), FOREIGN KEY(plan_id) REFERENCES activation_plans(id) ON DELETE RESTRICT
|
|
378
|
+
) STRICT, WITHOUT ROWID;
|
|
379
|
+
DELETE FROM operation_receipts WHERE operation IN ('create-plan', 'approve-plan', 'activate-plan');
|
|
380
|
+
PRAGMA user_version = 4;
|
|
381
|
+
`);
|
|
382
|
+
database.exec('COMMIT');
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
database.exec('ROLLBACK');
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function migrateV4ToV5(database) {
|
|
390
|
+
database.exec(`
|
|
391
|
+
BEGIN IMMEDIATE;
|
|
392
|
+
ALTER TABLE activation_plans ADD COLUMN activation_target_existed INTEGER CHECK(activation_target_existed IN (0, 1));
|
|
393
|
+
CREATE UNIQUE INDEX activation_plans_active_target ON activation_plans(target_path) WHERE status IN (
|
|
394
|
+
'staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-effect-blocked-replay',
|
|
395
|
+
'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health',
|
|
396
|
+
'commit-pending', 'rollback-pending'
|
|
397
|
+
);
|
|
398
|
+
PRAGMA user_version = 5;
|
|
399
|
+
COMMIT;
|
|
400
|
+
`);
|
|
401
|
+
}
|
|
402
|
+
function migrateV5ToV6(database) {
|
|
403
|
+
const rows = database.prepare('SELECT * FROM activation_plans').all();
|
|
404
|
+
database.exec('BEGIN IMMEDIATE');
|
|
405
|
+
try {
|
|
406
|
+
const quarantine = database.prepare('INSERT OR REPLACE INTO quarantined_legacy_plans (source, reason, payload_json, quarantined_at) VALUES (?, ?, ?, ?)');
|
|
407
|
+
rows.forEach((row, index) => quarantine.run(`activation_plans_v5:${index}`, 'activation-plan-lacks-ledger-path-and-executor-file-digest-binding', JSON.stringify(row), Date.now()));
|
|
408
|
+
for (const row of rows) {
|
|
409
|
+
const gapId = row['gap_id'];
|
|
410
|
+
if (typeof gapId !== 'string' || gapId === '')
|
|
411
|
+
throw new Error('legacy activation plan has an invalid gap id');
|
|
412
|
+
database.prepare(`UPDATE capability_gaps SET status = 'open', candidate_id = NULL, revision = revision + 1,
|
|
413
|
+
updated_at = ? WHERE id = ? AND status = 'matched'`).run(Date.now(), gapId);
|
|
414
|
+
}
|
|
415
|
+
database.exec(`
|
|
416
|
+
DELETE FROM operation_receipts WHERE json_extract(result_json, '$.result.id') IN (SELECT id FROM activation_plans);
|
|
417
|
+
DELETE FROM host_attestations;
|
|
418
|
+
DELETE FROM gap_plan_claims WHERE plan_kind = 'activation';
|
|
419
|
+
DELETE FROM activation_plans;
|
|
420
|
+
ALTER TABLE activation_plans ADD COLUMN ledger_id TEXT NOT NULL DEFAULT '';
|
|
421
|
+
ALTER TABLE activation_plans ADD COLUMN ledger_path TEXT NOT NULL DEFAULT '';
|
|
422
|
+
ALTER TABLE activation_plans ADD COLUMN executor_path TEXT NOT NULL DEFAULT '';
|
|
423
|
+
ALTER TABLE activation_plans ADD COLUMN executor_digest TEXT NOT NULL DEFAULT '';
|
|
424
|
+
PRAGMA user_version = 6;
|
|
425
|
+
`);
|
|
426
|
+
database.exec('COMMIT');
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
database.exec('ROLLBACK');
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function migrateV6ToV7(database) {
|
|
434
|
+
database.exec(`
|
|
435
|
+
BEGIN IMMEDIATE;
|
|
436
|
+
CREATE TABLE host_attestation_operations (
|
|
437
|
+
plan_id TEXT NOT NULL,
|
|
438
|
+
phase TEXT NOT NULL CHECK(phase IN ('reload', 'readiness', 'effect-blocked-replay', 'shadow', 'canary', 'soak', 'health')),
|
|
439
|
+
operation_id TEXT NOT NULL UNIQUE,
|
|
440
|
+
binding_digest TEXT NOT NULL CHECK(length(binding_digest) = 64),
|
|
441
|
+
request_digest TEXT NOT NULL CHECK(length(request_digest) = 64),
|
|
442
|
+
request_json TEXT NOT NULL CHECK(json_valid(request_json) AND json_type(request_json) = 'object'),
|
|
443
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'completed', 'applied')),
|
|
444
|
+
receipt_digest TEXT CHECK(receipt_digest IS NULL OR length(receipt_digest) = 64),
|
|
445
|
+
receipt_json TEXT CHECK(receipt_json IS NULL OR (json_valid(receipt_json) AND json_type(receipt_json) = 'object')),
|
|
446
|
+
created_at INTEGER NOT NULL,
|
|
447
|
+
completed_at INTEGER,
|
|
448
|
+
applied_at INTEGER,
|
|
449
|
+
PRIMARY KEY(plan_id, phase),
|
|
450
|
+
FOREIGN KEY(plan_id) REFERENCES activation_plans(id) ON DELETE RESTRICT
|
|
451
|
+
) STRICT, WITHOUT ROWID;
|
|
452
|
+
PRAGMA user_version = 7;
|
|
453
|
+
COMMIT;
|
|
454
|
+
`);
|
|
455
|
+
}
|
|
456
|
+
function migrateV7ToV8(database) {
|
|
457
|
+
database.exec(`
|
|
458
|
+
PRAGMA foreign_keys = OFF;
|
|
459
|
+
BEGIN IMMEDIATE;
|
|
460
|
+
ALTER TABLE source_plans RENAME TO source_plans_v7;
|
|
461
|
+
CREATE TABLE source_plans (
|
|
462
|
+
id TEXT PRIMARY KEY,
|
|
463
|
+
plan_digest TEXT NOT NULL UNIQUE CHECK(length(plan_digest) = 64),
|
|
464
|
+
gap_id TEXT NOT NULL,
|
|
465
|
+
gap_snapshot_json TEXT NOT NULL CHECK(json_valid(gap_snapshot_json) AND json_type(gap_snapshot_json) = 'object'),
|
|
466
|
+
repository TEXT NOT NULL,
|
|
467
|
+
worktree TEXT NOT NULL,
|
|
468
|
+
base_commit TEXT NOT NULL CHECK(length(base_commit) = 40),
|
|
469
|
+
plugin_name TEXT NOT NULL,
|
|
470
|
+
generator_digest TEXT NOT NULL CHECK(length(generator_digest) = 64),
|
|
471
|
+
scope_json TEXT NOT NULL CHECK(json_valid(scope_json) AND json_type(scope_json) = 'array'),
|
|
472
|
+
status TEXT NOT NULL CHECK(status IN (
|
|
473
|
+
'pending-approval', 'approved', 'running-local-checks', 'ready-for-human-review', 'local-checks-failed',
|
|
474
|
+
'awaiting-pr', 'awaiting-review', 'awaiting-merge', 'awaiting-build', 'awaiting-sign', 'awaiting-publish',
|
|
475
|
+
'awaiting-registry-verify', 'awaiting-catalog-admission', 'release-complete', 'release-failed', 'publish-ambiguous'
|
|
476
|
+
)),
|
|
477
|
+
revision INTEGER NOT NULL CHECK(revision >= 1),
|
|
478
|
+
created_at INTEGER NOT NULL,
|
|
479
|
+
expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
|
|
480
|
+
approval_json TEXT CHECK(approval_json IS NULL OR json_valid(approval_json)),
|
|
481
|
+
release_id TEXT,
|
|
482
|
+
release_fence INTEGER NOT NULL DEFAULT 0 CHECK(release_fence >= 0),
|
|
483
|
+
release_failure_phase TEXT CHECK(release_failure_phase IS NULL OR release_failure_phase IN (
|
|
484
|
+
'pr', 'review', 'merge', 'build', 'sign', 'publish', 'registry-verify', 'catalog-admission'
|
|
485
|
+
)),
|
|
486
|
+
release_failure_code TEXT,
|
|
487
|
+
updated_at INTEGER NOT NULL,
|
|
488
|
+
FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
|
|
489
|
+
) STRICT;
|
|
490
|
+
INSERT INTO source_plans (id, plan_digest, gap_id, gap_snapshot_json, repository, worktree, base_commit,
|
|
491
|
+
plugin_name, generator_digest, scope_json, status, revision, created_at, expires_at, approval_json,
|
|
492
|
+
release_id, release_fence, release_failure_phase, release_failure_code, updated_at)
|
|
493
|
+
SELECT id, plan_digest, gap_id, gap_snapshot_json, repository, worktree, base_commit, plugin_name,
|
|
494
|
+
generator_digest, scope_json, status, revision, created_at, expires_at, approval_json,
|
|
495
|
+
NULL, 0, NULL, NULL, updated_at FROM source_plans_v7;
|
|
496
|
+
DROP TABLE source_plans_v7;
|
|
497
|
+
CREATE TABLE source_release_operations (
|
|
498
|
+
plan_id TEXT NOT NULL,
|
|
499
|
+
phase TEXT NOT NULL CHECK(phase IN ('pr', 'review', 'merge', 'build', 'sign', 'publish', 'registry-verify', 'catalog-admission')),
|
|
500
|
+
operation_id TEXT NOT NULL UNIQUE,
|
|
501
|
+
binding_digest TEXT NOT NULL CHECK(length(binding_digest) = 64),
|
|
502
|
+
request_digest TEXT NOT NULL CHECK(length(request_digest) = 64),
|
|
503
|
+
request_json TEXT NOT NULL CHECK(json_valid(request_json) AND json_type(request_json) = 'object'),
|
|
504
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'completed', 'applied')),
|
|
505
|
+
receipt_digest TEXT CHECK(receipt_digest IS NULL OR length(receipt_digest) = 64),
|
|
506
|
+
receipt_json TEXT CHECK(receipt_json IS NULL OR (json_valid(receipt_json) AND json_type(receipt_json) = 'object')),
|
|
507
|
+
created_at INTEGER NOT NULL,
|
|
508
|
+
completed_at INTEGER,
|
|
509
|
+
applied_at INTEGER,
|
|
510
|
+
PRIMARY KEY(plan_id, phase),
|
|
511
|
+
FOREIGN KEY(plan_id) REFERENCES source_plans(id) ON DELETE RESTRICT
|
|
512
|
+
) STRICT, WITHOUT ROWID;
|
|
513
|
+
PRAGMA user_version = 8;
|
|
514
|
+
COMMIT;
|
|
515
|
+
PRAGMA foreign_keys = ON;
|
|
516
|
+
`);
|
|
517
|
+
}
|
|
518
|
+
function migrateV8ToV9(database) {
|
|
519
|
+
const sourceColumns = new Set(database.prepare('PRAGMA table_info(source_plans)').all().map(column => column.name));
|
|
520
|
+
const addSourceColumns = [
|
|
521
|
+
sourceColumns.has('checked_tree_digest') ? '' : "ALTER TABLE source_plans ADD COLUMN checked_tree_digest TEXT CHECK(checked_tree_digest IS NULL OR (length(checked_tree_digest) = 64 AND checked_tree_digest NOT GLOB '*[^a-f0-9]*'));",
|
|
522
|
+
sourceColumns.has('checked_patch_digest') ? '' : "ALTER TABLE source_plans ADD COLUMN checked_patch_digest TEXT CHECK(checked_patch_digest IS NULL OR (length(checked_patch_digest) = 64 AND checked_patch_digest NOT GLOB '*[^a-f0-9]*'));",
|
|
523
|
+
sourceColumns.has('checked_at') ? '' : 'ALTER TABLE source_plans ADD COLUMN checked_at INTEGER;',
|
|
524
|
+
sourceColumns.has('release_authorization_json') ? '' : `ALTER TABLE source_plans ADD COLUMN release_authorization_json TEXT
|
|
525
|
+
CHECK(release_authorization_json IS NULL OR (json_valid(release_authorization_json) AND json_type(release_authorization_json) = 'object'));`,
|
|
526
|
+
sourceColumns.has('release_authorization_digest') ? '' : 'ALTER TABLE source_plans ADD COLUMN release_authorization_digest TEXT CHECK(release_authorization_digest IS NULL OR length(release_authorization_digest) = 64);',
|
|
527
|
+
].join('\n');
|
|
528
|
+
database.exec(`
|
|
529
|
+
PRAGMA foreign_keys = OFF;
|
|
530
|
+
BEGIN IMMEDIATE;
|
|
531
|
+
-- The table did not exist in released v8. Keeping this cleanup inside the
|
|
532
|
+
-- transaction also makes interrupted-development migrations atomic.
|
|
533
|
+
DROP TABLE IF EXISTS source_publish_reconciliations;
|
|
534
|
+
${addSourceColumns}
|
|
535
|
+
UPDATE source_plans SET status = 'release-failed', release_failure_code = 'legacy-unverifiable-release',
|
|
536
|
+
release_failure_phase = CASE status
|
|
537
|
+
WHEN 'awaiting-pr' THEN 'pr' WHEN 'awaiting-review' THEN 'review' WHEN 'awaiting-merge' THEN 'merge'
|
|
538
|
+
WHEN 'awaiting-build' THEN 'build' WHEN 'awaiting-sign' THEN 'sign' WHEN 'awaiting-publish' THEN 'publish'
|
|
539
|
+
WHEN 'awaiting-registry-verify' THEN 'registry-verify' WHEN 'awaiting-catalog-admission' THEN 'catalog-admission'
|
|
540
|
+
WHEN 'publish-ambiguous' THEN 'publish' WHEN 'release-complete' THEN 'catalog-admission' ELSE release_failure_phase END,
|
|
541
|
+
revision = revision + 1, updated_at = CASE WHEN updated_at < created_at THEN created_at ELSE updated_at END
|
|
542
|
+
WHERE status IN ('awaiting-pr', 'awaiting-review', 'awaiting-merge', 'awaiting-build', 'awaiting-sign',
|
|
543
|
+
'awaiting-publish', 'awaiting-registry-verify', 'awaiting-catalog-admission', 'publish-ambiguous', 'release-complete');
|
|
544
|
+
ALTER TABLE source_release_operations RENAME TO source_release_operations_v8;
|
|
545
|
+
CREATE TABLE source_release_operations (
|
|
546
|
+
plan_id TEXT NOT NULL,
|
|
547
|
+
phase TEXT NOT NULL CHECK(phase IN ('pr', 'review', 'merge', 'build', 'sign', 'publish', 'registry-verify', 'catalog-admission')),
|
|
548
|
+
release_id TEXT NOT NULL,
|
|
549
|
+
release_fence INTEGER NOT NULL CHECK(release_fence >= 1),
|
|
550
|
+
attempt INTEGER NOT NULL CHECK(attempt >= 1),
|
|
551
|
+
operation_id TEXT NOT NULL UNIQUE,
|
|
552
|
+
binding_digest TEXT NOT NULL CHECK(length(binding_digest) = 64),
|
|
553
|
+
request_digest TEXT NOT NULL CHECK(length(request_digest) = 64),
|
|
554
|
+
request_json TEXT NOT NULL CHECK(json_valid(request_json) AND json_type(request_json) = 'object'),
|
|
555
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'completed', 'applied')),
|
|
556
|
+
receipt_digest TEXT CHECK(receipt_digest IS NULL OR length(receipt_digest) = 64),
|
|
557
|
+
receipt_json TEXT CHECK(receipt_json IS NULL OR (json_valid(receipt_json) AND json_type(receipt_json) = 'object')),
|
|
558
|
+
created_at INTEGER NOT NULL,
|
|
559
|
+
completed_at INTEGER,
|
|
560
|
+
applied_at INTEGER,
|
|
561
|
+
PRIMARY KEY(plan_id, phase, attempt),
|
|
562
|
+
UNIQUE(plan_id, phase, release_fence),
|
|
563
|
+
CHECK(
|
|
564
|
+
(status = 'pending' AND receipt_digest IS NULL AND receipt_json IS NULL AND completed_at IS NULL AND applied_at IS NULL) OR
|
|
565
|
+
(status = 'completed' AND receipt_digest IS NOT NULL AND receipt_json IS NOT NULL AND completed_at IS NOT NULL AND applied_at IS NULL) OR
|
|
566
|
+
(status = 'applied' AND receipt_digest IS NOT NULL AND receipt_json IS NOT NULL AND completed_at IS NOT NULL AND applied_at IS NOT NULL)
|
|
567
|
+
),
|
|
568
|
+
FOREIGN KEY(plan_id) REFERENCES source_plans(id) ON DELETE RESTRICT
|
|
569
|
+
) STRICT, WITHOUT ROWID;
|
|
570
|
+
INSERT OR REPLACE INTO quarantined_legacy_plans (source, reason, payload_json, quarantined_at)
|
|
571
|
+
SELECT 'source_release_operations_v8:' || operation_id, 'release-operation-lacks-post-check-authorization-and-attempt-binding',
|
|
572
|
+
json_object('plan_id', plan_id, 'phase', phase, 'operation_id', operation_id, 'binding_digest', binding_digest,
|
|
573
|
+
'request_digest', request_digest, 'request_json', json(request_json), 'status', status,
|
|
574
|
+
'receipt_digest', receipt_digest, 'receipt_json', CASE WHEN receipt_json IS NULL THEN NULL ELSE json(receipt_json) END,
|
|
575
|
+
'created_at', created_at, 'completed_at', completed_at, 'applied_at', applied_at),
|
|
576
|
+
CAST(strftime('%s', 'now') AS INTEGER) * 1000
|
|
577
|
+
FROM source_release_operations_v8;
|
|
578
|
+
DROP TABLE source_release_operations_v8;
|
|
579
|
+
CREATE TABLE source_publish_reconciliations (
|
|
580
|
+
plan_id TEXT NOT NULL,
|
|
581
|
+
release_id TEXT NOT NULL,
|
|
582
|
+
release_fence INTEGER NOT NULL CHECK(release_fence >= 1),
|
|
583
|
+
attempt INTEGER NOT NULL CHECK(attempt >= 1),
|
|
584
|
+
operation_id TEXT NOT NULL,
|
|
585
|
+
binding_digest TEXT NOT NULL CHECK(length(binding_digest) = 64),
|
|
586
|
+
request_digest TEXT NOT NULL CHECK(length(request_digest) = 64),
|
|
587
|
+
request_json TEXT NOT NULL CHECK(json_valid(request_json) AND json_type(request_json) = 'object'),
|
|
588
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'completed', 'applied')),
|
|
589
|
+
receipt_digest TEXT CHECK(receipt_digest IS NULL OR length(receipt_digest) = 64),
|
|
590
|
+
receipt_json TEXT CHECK(receipt_json IS NULL OR (json_valid(receipt_json) AND json_type(receipt_json) = 'object')),
|
|
591
|
+
created_at INTEGER NOT NULL,
|
|
592
|
+
completed_at INTEGER,
|
|
593
|
+
applied_at INTEGER,
|
|
594
|
+
PRIMARY KEY(plan_id, attempt),
|
|
595
|
+
UNIQUE(operation_id),
|
|
596
|
+
CHECK(
|
|
597
|
+
(status = 'pending' AND receipt_digest IS NULL AND receipt_json IS NULL AND completed_at IS NULL AND applied_at IS NULL) OR
|
|
598
|
+
(status = 'completed' AND receipt_digest IS NOT NULL AND receipt_json IS NOT NULL AND completed_at IS NOT NULL AND applied_at IS NULL) OR
|
|
599
|
+
(status = 'applied' AND receipt_digest IS NOT NULL AND receipt_json IS NOT NULL AND completed_at IS NOT NULL AND applied_at IS NOT NULL)
|
|
600
|
+
),
|
|
601
|
+
FOREIGN KEY(plan_id) REFERENCES source_plans(id) ON DELETE RESTRICT
|
|
602
|
+
) STRICT, WITHOUT ROWID;
|
|
603
|
+
CREATE INDEX source_publish_reconciliations_release ON source_publish_reconciliations(plan_id, release_fence, created_at);
|
|
604
|
+
PRAGMA user_version = 9;
|
|
605
|
+
COMMIT;
|
|
606
|
+
PRAGMA foreign_keys = ON;
|
|
607
|
+
`);
|
|
608
|
+
}
|
|
609
|
+
function migrateV9ToV10(database) {
|
|
610
|
+
database.exec('BEGIN IMMEDIATE');
|
|
611
|
+
try {
|
|
612
|
+
const columns = new Set(database.prepare('PRAGMA table_info(operation_receipts)').all().map(column => column.name));
|
|
613
|
+
const hadDigest = columns.has('result_digest');
|
|
614
|
+
const receipts = database.prepare(`SELECT idempotency_key, operation, input_digest, result_json,
|
|
615
|
+
${hadDigest ? 'result_digest' : 'NULL AS result_digest'}, created_at FROM operation_receipts`).all();
|
|
616
|
+
database.exec(`
|
|
617
|
+
ALTER TABLE operation_receipts RENAME TO operation_receipts_v9;
|
|
618
|
+
CREATE TABLE operation_receipts (
|
|
619
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
620
|
+
operation TEXT NOT NULL,
|
|
621
|
+
input_digest TEXT NOT NULL CHECK(length(input_digest) = 64),
|
|
622
|
+
result_json TEXT NOT NULL CHECK(json_valid(result_json)),
|
|
623
|
+
result_digest TEXT NOT NULL CHECK(length(result_digest) = 64),
|
|
624
|
+
created_at INTEGER NOT NULL
|
|
625
|
+
) STRICT, WITHOUT ROWID;
|
|
626
|
+
`);
|
|
627
|
+
const insert = database.prepare(`INSERT INTO operation_receipts
|
|
628
|
+
(idempotency_key, operation, input_digest, result_json, result_digest, created_at) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
629
|
+
const quarantine = database.prepare(`INSERT OR REPLACE INTO quarantined_legacy_plans
|
|
630
|
+
(source, reason, payload_json, quarantined_at) VALUES (?, ?, ?, ?)`);
|
|
631
|
+
for (const row of receipts) {
|
|
632
|
+
const digest = controlPlaneOperationReceiptDigest(row.idempotency_key, row.operation, row.input_digest, row.result_json, row.created_at);
|
|
633
|
+
if (!hadDigest || row.result_digest === null) {
|
|
634
|
+
quarantine.run('operation_receipts_v9:' + row.idempotency_key, 'operation-receipt-lacks-full-envelope-digest', JSON.stringify({ idempotencyKey: row.idempotency_key, operation: row.operation, inputDigest: row.input_digest,
|
|
635
|
+
result: JSON.parse(row.result_json), createdAt: row.created_at }), Date.now());
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (row.result_digest !== digest) {
|
|
639
|
+
throw new ControlPlaneDatabaseError('unsafe-file', 'schema-v9 operation receipt digest is corrupt');
|
|
640
|
+
}
|
|
641
|
+
insert.run(row.idempotency_key, row.operation, row.input_digest, row.result_json, digest, row.created_at);
|
|
642
|
+
}
|
|
643
|
+
database.exec('DROP TABLE operation_receipts_v9; PRAGMA user_version = 10; COMMIT');
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
database.exec('ROLLBACK');
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function migrateV10ToV11(database) {
|
|
651
|
+
const columns = new Set(database.prepare('PRAGMA table_info(activation_plans)').all().map(column => column.name));
|
|
652
|
+
database.exec('BEGIN IMMEDIATE');
|
|
653
|
+
try {
|
|
654
|
+
if (!columns.has('approval_receipt_json')) {
|
|
655
|
+
database.exec(`ALTER TABLE activation_plans ADD COLUMN approval_receipt_json TEXT
|
|
656
|
+
CHECK(approval_receipt_json IS NULL OR (json_valid(approval_receipt_json) AND json_type(approval_receipt_json) = 'object'))`);
|
|
657
|
+
}
|
|
658
|
+
database.exec('PRAGMA user_version = 11; COMMIT');
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
database.exec('ROLLBACK');
|
|
662
|
+
throw error;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
export function openControlPlaneDatabase(path) {
|
|
666
|
+
prepare(path);
|
|
667
|
+
const database = new DatabaseSync(path);
|
|
668
|
+
try {
|
|
669
|
+
database.exec('PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
|
|
670
|
+
const version = Number(database.prepare('PRAGMA user_version').get().user_version);
|
|
671
|
+
if (version > controlPlaneSchemaVersion)
|
|
672
|
+
throw new ControlPlaneDatabaseError('schema-too-new', 'plugin-control-plane database schema is newer than this binary');
|
|
673
|
+
if (version === 0)
|
|
674
|
+
createCurrent(database);
|
|
675
|
+
else if (version === 1)
|
|
676
|
+
migrateV1(database);
|
|
677
|
+
else {
|
|
678
|
+
if (version === 2)
|
|
679
|
+
migrateV2ToV3(database);
|
|
680
|
+
if (version <= 3)
|
|
681
|
+
migrateV3ToV4(database);
|
|
682
|
+
if (version <= 4)
|
|
683
|
+
migrateV4ToV5(database);
|
|
684
|
+
if (version <= 5)
|
|
685
|
+
migrateV5ToV6(database);
|
|
686
|
+
if (version <= 6)
|
|
687
|
+
migrateV6ToV7(database);
|
|
688
|
+
if (version <= 7)
|
|
689
|
+
migrateV7ToV8(database);
|
|
690
|
+
if (version <= 8)
|
|
691
|
+
migrateV8ToV9(database);
|
|
692
|
+
if (version <= 9)
|
|
693
|
+
migrateV9ToV10(database);
|
|
694
|
+
if (version <= 10)
|
|
695
|
+
migrateV10ToV11(database);
|
|
696
|
+
}
|
|
697
|
+
database.exec('PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;');
|
|
698
|
+
return database;
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
database.close();
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
//# sourceMappingURL=sqlite.js.map
|