@casecore/release-agent 0.1.4 → 0.1.7
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 +52 -1
- package/dist/exclusive-operation.mjs +3 -0
- package/dist/index.mjs +1 -1
- package/dist/producer-admission.mjs +3 -0
- package/dist/producer-delivery.mjs +2 -2
- package/dist/producer-execution.mjs +33 -67
- package/dist/producer-journal.mjs +44 -0
- package/dist/producer-publication-failure.mjs +69 -10
- package/dist/producer-server.mjs +29 -51
- package/dist/producer-state.mjs +22 -35
- package/dist/producer.d.ts +101 -5
- package/dist/producer.mjs +45 -1
- package/dist/provenance.json +16 -14
- package/dist/publication-terminal-receipt.mjs +119 -0
- package/dist/publication-terminal.mjs +251 -0
- package/package.json +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { ProducerState } from './producer-state.mjs';
|
|
3
|
+
import { assertProducerOperation } from './exclusive-operation.mjs';
|
|
4
|
+
import { canonical, hash } from './protocol-internals.mjs';
|
|
5
|
+
import { acceptedProduction, sameIdentity, validateExpectedSource } from './protocol.mjs';
|
|
6
|
+
import { verifyPublicationDelivery } from './producer-delivery.mjs';
|
|
7
|
+
import { validatePublicationFailureReadback } from './producer-publication-failure.mjs';
|
|
8
|
+
import { validateOriginalDelivery } from './producer-support.mjs';
|
|
9
|
+
import {
|
|
10
|
+
createPublicationTerminalReceipt,
|
|
11
|
+
verifyPublicationTerminalReceipt,
|
|
12
|
+
} from './publication-terminal-receipt.mjs';
|
|
13
|
+
export { verifyPublicationTerminalReceipt } from './publication-terminal-receipt.mjs';
|
|
14
|
+
|
|
15
|
+
const authorities = new WeakMap();
|
|
16
|
+
|
|
17
|
+
/** The capability is valid only inside the engine's terminal callback. */
|
|
18
|
+
export function readPublicationTerminalAuthority(authority, bindings) {
|
|
19
|
+
const record = authorities.get(authority);
|
|
20
|
+
assert.ok(record && Date.now() <= record.expiresAt, 'PUBLICATION_TERMINAL_AUTHORITY_REQUIRED');
|
|
21
|
+
assert.equal(
|
|
22
|
+
canonical(bindings),
|
|
23
|
+
canonical(record.proof.bindings),
|
|
24
|
+
'PUBLICATION_TERMINAL_BINDINGS_CHANGED'
|
|
25
|
+
);
|
|
26
|
+
return structuredClone(record.proof);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** DEP-TERMINAL-V1: mint only inside the genuine current engine callback. */
|
|
30
|
+
export async function sealPublicationTerminalReceipt(authority, bindings, token) {
|
|
31
|
+
const proof = readPublicationTerminalAuthority(authority, bindings);
|
|
32
|
+
const record = authorities.get(authority);
|
|
33
|
+
assertProducerOperation(record.producer);
|
|
34
|
+
assert.equal(
|
|
35
|
+
canonical(record.producer.journal.read().commands[bindings.commandId]),
|
|
36
|
+
canonical(record.item),
|
|
37
|
+
'PUBLICATION_TERMINAL_NOT_DURABLE'
|
|
38
|
+
);
|
|
39
|
+
assert.equal(
|
|
40
|
+
canonical(record.producer.journal.read().runs.find((run) => run.id === bindings.runId)),
|
|
41
|
+
record.durableRun,
|
|
42
|
+
'PUBLICATION_TERMINAL_NOT_DURABLE'
|
|
43
|
+
);
|
|
44
|
+
const receipt = createPublicationTerminalReceipt(proof, token);
|
|
45
|
+
const prior = record.item.deliveryTerminalReceipt;
|
|
46
|
+
if (prior) {
|
|
47
|
+
const existing = verifyPublicationTerminalReceipt(prior, token).proof;
|
|
48
|
+
for (const key of ['bindings', 'status', 'completedAt', 'artifactIdentity', 'failure'])
|
|
49
|
+
assert.equal(
|
|
50
|
+
canonical({ value: existing[key] }),
|
|
51
|
+
canonical({ value: proof[key] }),
|
|
52
|
+
'PUBLICATION_TERMINAL_RESULT_CHANGED'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
record.item.deliveryTerminalReceipt = structuredClone(receipt);
|
|
56
|
+
await record.producer.save();
|
|
57
|
+
readPublicationTerminalAuthority(authority, bindings);
|
|
58
|
+
return structuredClone(receipt);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fresh = (readback, now) =>
|
|
62
|
+
[readback.observedAt, readback.functionalSmoke.checkedAt].every((value) => {
|
|
63
|
+
const age = now.getTime() - Date.parse(value);
|
|
64
|
+
return age >= 0 && age <= 300000;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Internal issuance is deliberately absent from the package public exports.
|
|
68
|
+
export async function notifyPublicationTerminal(producer, item, run) {
|
|
69
|
+
if (
|
|
70
|
+
!['publish', 'deploy'].includes(item.command.action) ||
|
|
71
|
+
(item.command.action === 'deploy' && !item.deliveryExecution) ||
|
|
72
|
+
typeof producer.provider.publicationTerminal !== 'function'
|
|
73
|
+
)
|
|
74
|
+
return;
|
|
75
|
+
if (!['deployed', 'failed'].includes(run.status) || run.recoveryState !== 'verified') return;
|
|
76
|
+
assert.ok(ProducerState.isEngineInstance(producer), 'PUBLICATION_ENGINE_INSTANCE_REQUIRED');
|
|
77
|
+
assertProducerOperation(producer);
|
|
78
|
+
let authority;
|
|
79
|
+
let controller;
|
|
80
|
+
let timer;
|
|
81
|
+
try {
|
|
82
|
+
controller = new AbortController();
|
|
83
|
+
const expiresAt = Date.now() + 30000;
|
|
84
|
+
const timeout = new Promise((_, reject) => {
|
|
85
|
+
timer = setTimeout(() => {
|
|
86
|
+
controller.abort();
|
|
87
|
+
reject(Error('PUBLICATION_TERMINAL_CALLBACK_TIMEOUT'));
|
|
88
|
+
}, 30000);
|
|
89
|
+
});
|
|
90
|
+
const bounded = (operation) => Promise.race([operation, timeout]);
|
|
91
|
+
assert.ok(
|
|
92
|
+
Date.parse(run.completedAt) >= Date.parse(item.command.requestedAt) &&
|
|
93
|
+
Date.parse(run.completedAt) <= producer.now().getTime(),
|
|
94
|
+
'PUBLICATION_TERMINAL_COMPLETION_REQUIRED'
|
|
95
|
+
);
|
|
96
|
+
if (run.status === 'deployed')
|
|
97
|
+
assert.ok(
|
|
98
|
+
Date.parse(run.completedAt) <= Date.parse(item.deliveryExecution.deadlineAt),
|
|
99
|
+
'PUBLICATION_TERMINAL_COMPLETION_LATE'
|
|
100
|
+
);
|
|
101
|
+
const verification = {
|
|
102
|
+
signal: controller.signal,
|
|
103
|
+
completedAt: run.completedAt,
|
|
104
|
+
status: run.status,
|
|
105
|
+
artifactIdentity: item.artifactIdentity ?? item.command.expectedIdentity,
|
|
106
|
+
};
|
|
107
|
+
let source;
|
|
108
|
+
const artifactIdentity =
|
|
109
|
+
item.command.action === 'publish' ? item.artifactIdentity : item.command.expectedIdentity;
|
|
110
|
+
if (item.command.action === 'publish') {
|
|
111
|
+
source = item.command.expectedSource;
|
|
112
|
+
await bounded(
|
|
113
|
+
verifyPublicationDelivery(producer, item.command, item.deliveryExecution, {
|
|
114
|
+
fresh: false,
|
|
115
|
+
verification,
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
validateOriginalDelivery(item.deliveryExecution);
|
|
120
|
+
assert.equal(
|
|
121
|
+
Date.parse(item.command.requestedAt),
|
|
122
|
+
Date.parse(item.deliveryExecution.requestedAt)
|
|
123
|
+
);
|
|
124
|
+
assert.equal(
|
|
125
|
+
Date.parse(item.command.deadlineAt),
|
|
126
|
+
Date.parse(item.deliveryExecution.deadlineAt)
|
|
127
|
+
);
|
|
128
|
+
assert.equal(item.ack.state, 'accepted');
|
|
129
|
+
assert.equal(item.fingerprint, hash(canonical(item.command)));
|
|
130
|
+
assert.ok(
|
|
131
|
+
item.artifactCandidate && sameIdentity(item.artifactCandidate.identity, artifactIdentity),
|
|
132
|
+
'PUBLICATION_ARTIFACT_CANDIDATE_REQUIRED'
|
|
133
|
+
);
|
|
134
|
+
source = {
|
|
135
|
+
repository: item.artifactCandidate.source.repository,
|
|
136
|
+
ref: item.artifactCandidate.source.ref,
|
|
137
|
+
sourceCommit: artifactIdentity.sourceCommit,
|
|
138
|
+
};
|
|
139
|
+
assert.ok(producer.sourcePolicy?.repository, 'PUBLICATION_ARTIFACT_REPOSITORY_REQUIRED');
|
|
140
|
+
assert.match(source.ref, /^[\w./-]{1,200}$/);
|
|
141
|
+
// A feature branch admission policy does not describe the signed artifact's
|
|
142
|
+
// real main/tag source. The mandatory product verifier owns that lineage.
|
|
143
|
+
validateExpectedSource(source, {
|
|
144
|
+
repository: producer.sourcePolicy.repository,
|
|
145
|
+
ref: source.ref,
|
|
146
|
+
});
|
|
147
|
+
assert.equal(
|
|
148
|
+
typeof producer.provider.verifyArtifactDelivery,
|
|
149
|
+
'function',
|
|
150
|
+
'PUBLICATION_ARTIFACT_DELIVERY_VERIFIER_REQUIRED'
|
|
151
|
+
);
|
|
152
|
+
assert.equal(
|
|
153
|
+
await bounded(
|
|
154
|
+
producer.provider.verifyArtifactDelivery(
|
|
155
|
+
item.command,
|
|
156
|
+
artifactIdentity,
|
|
157
|
+
item.deliveryExecution,
|
|
158
|
+
structuredClone(source),
|
|
159
|
+
verification
|
|
160
|
+
)
|
|
161
|
+
),
|
|
162
|
+
true,
|
|
163
|
+
'PUBLICATION_LINEAGE_REQUIRED'
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const saved = producer.journal.read();
|
|
167
|
+
assert.equal(
|
|
168
|
+
canonical(saved.commands[item.command.commandId]),
|
|
169
|
+
canonical(item),
|
|
170
|
+
'PUBLICATION_TERMINAL_NOT_DURABLE'
|
|
171
|
+
);
|
|
172
|
+
assert.equal(
|
|
173
|
+
canonical(saved.runs.find((value) => value.id === run.id)),
|
|
174
|
+
canonical(run),
|
|
175
|
+
'PUBLICATION_TERMINAL_NOT_DURABLE'
|
|
176
|
+
);
|
|
177
|
+
if (run.status === 'deployed') {
|
|
178
|
+
assert.ok(artifactIdentity, 'PUBLICATION_ARTIFACT_REQUIRED');
|
|
179
|
+
assert.ok(
|
|
180
|
+
acceptedProduction(run.readback, producer.resolvedCommand(item.command), producer.now()),
|
|
181
|
+
'PUBLICATION_TERMINAL_READBACK_REQUIRED'
|
|
182
|
+
);
|
|
183
|
+
assert.ok(fresh(run.readback, producer.now()), 'PUBLICATION_TERMINAL_READBACK_STALE');
|
|
184
|
+
if (item.command.action === 'publish')
|
|
185
|
+
assert.equal(
|
|
186
|
+
await bounded(
|
|
187
|
+
producer.provider.verifyPublicationArtifact(
|
|
188
|
+
item.command,
|
|
189
|
+
artifactIdentity,
|
|
190
|
+
item.deliveryExecution,
|
|
191
|
+
verification
|
|
192
|
+
)
|
|
193
|
+
),
|
|
194
|
+
true,
|
|
195
|
+
'PUBLICATION_LINEAGE_REQUIRED'
|
|
196
|
+
);
|
|
197
|
+
} else {
|
|
198
|
+
// The existing independently owned pre-install failure guard remains the
|
|
199
|
+
// sole failure authority until artifact recovery has an equivalent guard.
|
|
200
|
+
assert.equal(
|
|
201
|
+
item.publicationFailureReconciliation?.state,
|
|
202
|
+
'settled',
|
|
203
|
+
'PUBLICATION_FAILURE_NOT_SETTLED'
|
|
204
|
+
);
|
|
205
|
+
validatePublicationFailureReadback(run.readback, item.command, item.baseline, producer.now());
|
|
206
|
+
}
|
|
207
|
+
const bindings = {
|
|
208
|
+
action: item.command.action,
|
|
209
|
+
commandId: item.command.commandId,
|
|
210
|
+
runId: run.id,
|
|
211
|
+
installationKey: item.command.installationKey,
|
|
212
|
+
repository: source.repository,
|
|
213
|
+
ref: source.ref,
|
|
214
|
+
provider: item.command.executionProvider,
|
|
215
|
+
candidateId: item.command.candidateId,
|
|
216
|
+
delivery: structuredClone(item.deliveryExecution),
|
|
217
|
+
};
|
|
218
|
+
const proof = {
|
|
219
|
+
schema: 1,
|
|
220
|
+
bindings,
|
|
221
|
+
status: run.status,
|
|
222
|
+
completedAt: run.completedAt,
|
|
223
|
+
artifactIdentity: structuredClone(artifactIdentity ?? null),
|
|
224
|
+
readback: structuredClone(run.readback),
|
|
225
|
+
failure: structuredClone(item.publicationFailureReconciliation?.fact ?? null),
|
|
226
|
+
};
|
|
227
|
+
authority = Object.freeze(Object.create(null));
|
|
228
|
+
controller.signal.throwIfAborted();
|
|
229
|
+
authorities.set(authority, { proof, expiresAt, producer, item, durableRun: canonical(run) });
|
|
230
|
+
await bounded(
|
|
231
|
+
Promise.resolve().then(() =>
|
|
232
|
+
producer.provider.publicationTerminal(structuredClone(item.command), {
|
|
233
|
+
authority,
|
|
234
|
+
bindings: structuredClone(bindings),
|
|
235
|
+
signal: controller.signal,
|
|
236
|
+
})
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
// The deployment result remains truthful. Product origin closure is unproven
|
|
241
|
+
// and must retry through fresh official reconciliation, never a raw snapshot.
|
|
242
|
+
producer.diagnostic(run.id, null, error);
|
|
243
|
+
await producer.save();
|
|
244
|
+
return false;
|
|
245
|
+
} finally {
|
|
246
|
+
if (authority) authorities.delete(authority);
|
|
247
|
+
controller?.abort();
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|