@casecore/release-agent 0.1.1 → 0.1.2
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 +27 -2
- package/dist/index.mjs +1 -0
- package/dist/producer-journal.mjs +20 -2
- package/dist/producer-publication-failure.mjs +87 -8
- package/dist/producer.d.ts +5 -0
- package/dist/producer.mjs +1 -0
- package/dist/provenance.json +8 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,19 +61,42 @@ UUID исходной попытки доставки и UUID команды Cas
|
|
|
61
61
|
`reconcile(commandId)` вызывает подключённый
|
|
62
62
|
`provider.reconcilePublicationFailure(command, { signal, delivery, recoveryDeadlineAt })`.
|
|
63
63
|
Адаптер возвращает `null` при неопределённом результате либо удерживаемый guard:
|
|
64
|
-
`{ fact, assertOwned(), release({ resolved, proofDigest }) }`.
|
|
64
|
+
`{ fact, baselineReadback, assertOwned(), release({ resolved, proofDigest }) }`.
|
|
65
65
|
`fact` содержит ровно `schema: 1`, `kind: 'failed_before_installation'`,
|
|
66
66
|
`commandId`, `installationKey`, `expectedSource`, исходный `delivery`, свежий
|
|
67
67
|
`observedAt`, `stage: source_ready | ci | build | verify | publish`,
|
|
68
68
|
`authenticated: true`, `executionStopped: true`, `pendingTransaction: false`,
|
|
69
69
|
`publicationResolved: true`, `installationNotStarted: true`.
|
|
70
70
|
|
|
71
|
+
`baselineReadback` использует существующий формат `ProductionReadback` и
|
|
72
|
+
подтверждает предыдущую рабочую установку под тем же удерживаемым guard.
|
|
73
|
+
Её точная идентичность обязана совпадать с baseline, сохранённым при принятии
|
|
74
|
+
исходной команды. Требуются правильная установка, `source: independent-production`,
|
|
75
|
+
успешный functional smoke с тем же source commit и ссылкой на доказательство.
|
|
76
|
+
`observedAt` и `functionalSmoke.checkedAt` не предшествуют исходному T0,
|
|
77
|
+
не находятся в будущем и отстают от завершения не более чем на 300 секунд.
|
|
78
|
+
Отсутствующий baseline, readback или несовпадение оставляют восстановление pending.
|
|
79
|
+
|
|
71
80
|
Адаптер подтверждает фактическую остановку исполнителя и его дочерних/удалённых
|
|
72
81
|
операций, исходную идентичность и отсутствие незавершённой установки через
|
|
73
82
|
существующие журналы и механизмы владения продукта. Он удерживает исключение
|
|
74
83
|
конкурирующих действий до сохранения pending-доказательства и подтверждения
|
|
75
84
|
его durable-завершения в `release({ resolved: true, proofDigest })`.
|
|
76
|
-
|
|
85
|
+
До ACK движок сохраняет точные `fact` и `baselineReadback` в защищённом pending-журнале,
|
|
86
|
+
а readback — в существующем `run.readback`; статус остаётся `blocked`,
|
|
87
|
+
`recoveryState: unverified`, `completedAt: null`.
|
|
88
|
+
Адаптер вычисляет ожидаемый ACK digest общей функцией
|
|
89
|
+
`publicationFailureProofDigest(fact, baselineReadback)` из `@casecore/release-agent`
|
|
90
|
+
или `@casecore/release-agent/producer`. Движок использует ту же функцию;
|
|
91
|
+
копировать реализацию хеша в адаптер не нужно. Функция вычисляет digest,
|
|
92
|
+
а доверенность доказательств и владение проверяются отдельно.
|
|
93
|
+
`proofDigest` связывает оба значения: `sha256:${hash(canonical({ fact, baselineReadback }))}`.
|
|
94
|
+
Здесь `canonical` сортирует ключи верхнего уровня объекта через `localeCompare`
|
|
95
|
+
и возвращает его JSON-строку; `hash` вычисляет SHA-256 от `JSON.stringify`
|
|
96
|
+
этой строки в UTF-8. Вложенные значения сохраняют точный порядок ключей
|
|
97
|
+
переданного адаптером доказательства. Адаптер подтверждает тот же digest.
|
|
98
|
+
Владение и свежесть повторно проверяются перед ACK, а после ACK — локальное
|
|
99
|
+
владение и свежесть перед сохранением завершения.
|
|
77
100
|
Ошибка/потеря ACK не разрешает следующий выпуск. Guard и его callback работают
|
|
78
101
|
внутри доверенного адаптера; HTTP-запрос не может подставить этот результат.
|
|
79
102
|
|
|
@@ -82,6 +105,8 @@ UUID исходной попытки доставки и UUID команды Cas
|
|
|
82
105
|
этапы. Артефакт и успешная установка не подставляются. Сохранённый pending
|
|
83
106
|
запрещает команды, продолжение и резервирование после перезапуска; восстановление
|
|
84
107
|
проходит через тот же `reconcile` и новую фактическую проверку владения.
|
|
108
|
+
Повторная проверка может обновить время наблюдения, но не исходный baseline.
|
|
109
|
+
Старое завершённое восстановление без readback не превращается в доказанный результат.
|
|
85
110
|
Его отдельный предел — 30 секунд; срок публикации не продлевается.
|
|
86
111
|
Обычный `reserveExclusiveOperation` становится доступен после завершения
|
|
87
112
|
восстановления, когда остальные операции установки также разрешены.
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { hash, canonical, keys, uuid } from './protocol-internals.mjs';
|
|
3
3
|
import { validateRunnerCommand, validateProducerSnapshot } from './protocol.mjs';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
validatePublicationFailureRecord,
|
|
6
|
+
validatePublicationFailureReadback,
|
|
7
|
+
} from './producer-publication-failure.mjs';
|
|
5
8
|
|
|
6
9
|
/** Validate persisted state before any provider command can run after restart. */
|
|
7
10
|
export function validProducerState(state, installationKey, sourcePolicy) {
|
|
@@ -30,7 +33,8 @@ export function validProducerState(state, installationKey, sourcePolicy) {
|
|
|
30
33
|
validatePublicationFailureRecord(
|
|
31
34
|
item.publicationFailureReconciliation,
|
|
32
35
|
item.command,
|
|
33
|
-
item.deliveryExecution
|
|
36
|
+
item.deliveryExecution,
|
|
37
|
+
item.baseline
|
|
34
38
|
);
|
|
35
39
|
}
|
|
36
40
|
validateProducerSnapshot(
|
|
@@ -58,7 +62,21 @@ export function validProducerState(state, installationKey, sourcePolicy) {
|
|
|
58
62
|
if (reconciliation.state === 'settled') {
|
|
59
63
|
assert.equal(run.status, 'failed');
|
|
60
64
|
assert.equal(run.recoveryState, 'verified');
|
|
65
|
+
assert.ok(run.completedAt);
|
|
66
|
+
validatePublicationFailureReadback(
|
|
67
|
+
reconciliation.baselineReadback,
|
|
68
|
+
state.commands[run.commandId].command,
|
|
69
|
+
state.commands[run.commandId].baseline,
|
|
70
|
+
new Date(run.completedAt)
|
|
71
|
+
);
|
|
61
72
|
} else assert.equal(run.status, 'blocked');
|
|
73
|
+
if (reconciliation.baselineReadback != null) {
|
|
74
|
+
assert.deepEqual(run.readback, reconciliation.baselineReadback);
|
|
75
|
+
if (reconciliation.state === 'pending') {
|
|
76
|
+
assert.equal(run.recoveryState, 'unverified');
|
|
77
|
+
assert.equal(run.completedAt, null);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
62
80
|
}
|
|
63
81
|
}
|
|
64
82
|
return true;
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
canonical,
|
|
4
|
+
hash,
|
|
5
|
+
keys,
|
|
6
|
+
timestamp,
|
|
7
|
+
completeIdentity,
|
|
8
|
+
validateReadback,
|
|
9
|
+
} from './protocol-internals.mjs';
|
|
10
|
+
import { acceptedProduction, sameIdentity } from './protocol.mjs';
|
|
3
11
|
import { validateOriginalDelivery, blocker } from './producer-support.mjs';
|
|
4
12
|
import { verifyPublicationDelivery } from './producer-delivery.mjs';
|
|
5
13
|
import { reservePublicationFailureReconciliation } from './exclusive-operation.mjs';
|
|
6
14
|
|
|
7
15
|
const stages = ['source_ready', 'ci', 'build', 'verify', 'publish'];
|
|
16
|
+
export function publicationFailureProofDigest(fact, baselineReadback) {
|
|
17
|
+
return `sha256:${hash(canonical({ fact, baselineReadback }))}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
export function validatePublicationFailureFact(fact, command, delivery) {
|
|
9
21
|
keys(fact, [
|
|
10
22
|
'schema',
|
|
@@ -44,11 +56,45 @@ export function validatePublicationFailureFact(fact, command, delivery) {
|
|
|
44
56
|
return fact;
|
|
45
57
|
}
|
|
46
58
|
|
|
47
|
-
export function
|
|
48
|
-
|
|
59
|
+
export function validatePublicationFailureReadback(readback, command, baseline, now) {
|
|
60
|
+
assert.ok(completeIdentity(baseline?.identity), 'Retained production baseline required');
|
|
61
|
+
validateReadback(baseline);
|
|
62
|
+
assert.equal(baseline.installationKey, command.installationKey);
|
|
63
|
+
validateReadback(readback);
|
|
64
|
+
assert.ok(sameIdentity(readback.identity, baseline.identity), 'Production baseline changed');
|
|
65
|
+
assert.equal(readback.installationKey, command.installationKey);
|
|
66
|
+
assert.equal(readback.source, 'independent-production');
|
|
67
|
+
assert.equal(readback.functionalSmoke.status, 'passed');
|
|
68
|
+
assert.equal(readback.functionalSmoke.sourceCommit, baseline.identity.sourceCommit);
|
|
69
|
+
const observations = [readback.observedAt, readback.functionalSmoke.checkedAt];
|
|
70
|
+
assert.ok(
|
|
71
|
+
observations.every((value) => Date.parse(value) >= Date.parse(command.requestedAt)),
|
|
72
|
+
'Baseline observations must preserve the original request boundary'
|
|
73
|
+
);
|
|
74
|
+
if (now) {
|
|
75
|
+
assert.ok(
|
|
76
|
+
acceptedProduction(readback, { ...command, expectedIdentity: baseline.identity }, now),
|
|
77
|
+
'Independent production baseline acceptance required'
|
|
78
|
+
);
|
|
79
|
+
assert.ok(
|
|
80
|
+
observations.every((value) => now.getTime() - Date.parse(value) <= 300000),
|
|
81
|
+
'Fresh production baseline readback required'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return readback;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function validatePublicationFailureRecord(record, command, delivery, baseline) {
|
|
88
|
+
keys(record, ['state', 'fact'], ['baselineReadback']);
|
|
49
89
|
assert.ok(['pending', 'settled'].includes(record.state));
|
|
50
90
|
assert.ok(record.state === 'pending' || record.fact !== null);
|
|
51
91
|
if (record.fact !== null) validatePublicationFailureFact(record.fact, command, delivery);
|
|
92
|
+
if (record.baselineReadback != null) {
|
|
93
|
+
assert.ok(record.fact, 'Baseline proof requires its publication failure fact');
|
|
94
|
+
validatePublicationFailureReadback(record.baselineReadback, command, baseline);
|
|
95
|
+
}
|
|
96
|
+
if (record.state === 'settled')
|
|
97
|
+
assert.ok(record.baselineReadback, 'Settled recovery requires its production baseline proof');
|
|
52
98
|
}
|
|
53
99
|
|
|
54
100
|
export async function reconcilePublicationFailure(producer, item, run) {
|
|
@@ -86,8 +132,14 @@ export async function reconcilePublicationFailure(producer, item, run) {
|
|
|
86
132
|
.then(() => owned.release({ resolved: false, proofDigest: null }))
|
|
87
133
|
.catch(() => {});
|
|
88
134
|
try {
|
|
89
|
-
item.publicationFailureReconciliation ??= {
|
|
135
|
+
item.publicationFailureReconciliation ??= {
|
|
136
|
+
state: 'pending',
|
|
137
|
+
fact: null,
|
|
138
|
+
baselineReadback: null,
|
|
139
|
+
};
|
|
90
140
|
assert.equal(item.publicationFailureReconciliation.state, 'pending');
|
|
141
|
+
run.recoveryState = 'unverified';
|
|
142
|
+
run.completedAt = null;
|
|
91
143
|
await bounded(producer.save());
|
|
92
144
|
const arriving = Promise.resolve().then(() =>
|
|
93
145
|
producer.provider.reconcilePublicationFailure(structuredClone(item.command), {
|
|
@@ -113,8 +165,26 @@ export async function reconcilePublicationFailure(producer, item, run) {
|
|
|
113
165
|
const fact = structuredClone(
|
|
114
166
|
validatePublicationFailureFact(guard.fact, item.command, item.deliveryExecution)
|
|
115
167
|
);
|
|
116
|
-
const
|
|
117
|
-
|
|
168
|
+
const baselineReadback = structuredClone(
|
|
169
|
+
validatePublicationFailureReadback(
|
|
170
|
+
guard.baselineReadback,
|
|
171
|
+
item.command,
|
|
172
|
+
item.baseline,
|
|
173
|
+
producer.now()
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
const assertFresh = () => {
|
|
177
|
+
const now = producer.now();
|
|
178
|
+
assert.ok(
|
|
179
|
+
now.getTime() <= recoveryDeadlineAt,
|
|
180
|
+
'Publication failure recovery deadline expired'
|
|
181
|
+
);
|
|
182
|
+
const age = now.getTime() - Date.parse(fact.observedAt);
|
|
183
|
+
assert.ok(age >= -30000 && age <= 30000, 'Fresh publication failure authority required');
|
|
184
|
+
validatePublicationFailureReadback(baselineReadback, item.command, item.baseline, now);
|
|
185
|
+
return now;
|
|
186
|
+
};
|
|
187
|
+
assertFresh();
|
|
118
188
|
const row = run.stages.find((stage) => stage.id === fact.stage);
|
|
119
189
|
assert.ok(
|
|
120
190
|
row && !['passed', 'reused'].includes(row.status),
|
|
@@ -132,18 +202,25 @@ export async function reconcilePublicationFailure(producer, item, run) {
|
|
|
132
202
|
await bounded(guard.assertOwned());
|
|
133
203
|
lease.assertOwned();
|
|
134
204
|
controller.signal.throwIfAborted();
|
|
205
|
+
assertFresh();
|
|
135
206
|
item.publicationFailureReconciliation.fact = fact;
|
|
207
|
+
item.publicationFailureReconciliation.baselineReadback = baselineReadback;
|
|
136
208
|
item.deliveryExecution = structuredClone(fact.delivery);
|
|
209
|
+
run.readback = structuredClone(baselineReadback);
|
|
137
210
|
await bounded(producer.save());
|
|
138
|
-
const proofDigest =
|
|
211
|
+
const proofDigest = publicationFailureProofDigest(fact, baselineReadback);
|
|
139
212
|
await bounded(guard.assertOwned());
|
|
213
|
+
lease.assertOwned();
|
|
214
|
+
controller.signal.throwIfAborted();
|
|
215
|
+
assertFresh();
|
|
140
216
|
releasing = true;
|
|
141
217
|
await bounded(guard.release({ resolved: true, proofDigest }));
|
|
142
218
|
controller.signal.throwIfAborted();
|
|
143
219
|
lease.assertOwned();
|
|
220
|
+
const completedAt = assertFresh().toISOString();
|
|
144
221
|
run.status = 'failed';
|
|
145
222
|
run.recoveryState = 'verified';
|
|
146
|
-
run.completedAt =
|
|
223
|
+
run.completedAt = completedAt;
|
|
147
224
|
run.blockers = [
|
|
148
225
|
blocker(
|
|
149
226
|
'FAILED_BEFORE_INSTALLATION',
|
|
@@ -166,6 +243,8 @@ export async function reconcilePublicationFailure(producer, item, run) {
|
|
|
166
243
|
item.publicationFailureReconciliation ??= { state: 'pending', fact: null };
|
|
167
244
|
item.publicationFailureReconciliation.state = 'pending';
|
|
168
245
|
run.status = 'blocked';
|
|
246
|
+
run.recoveryState = 'unverified';
|
|
247
|
+
run.completedAt = null;
|
|
169
248
|
run.blockers = [
|
|
170
249
|
blocker(
|
|
171
250
|
'RECONCILIATION_REQUIRED',
|
package/dist/producer.d.ts
CHANGED
|
@@ -98,9 +98,14 @@ export interface PublicationFailureFact {
|
|
|
98
98
|
}
|
|
99
99
|
export interface PublicationFailureGuard {
|
|
100
100
|
fact: PublicationFailureFact;
|
|
101
|
+
baselineReadback: ProductionReadback;
|
|
101
102
|
assertOwned(): Promise<void>;
|
|
102
103
|
release(options: { resolved: boolean; proofDigest: string | null }): Promise<void>;
|
|
103
104
|
}
|
|
105
|
+
export function publicationFailureProofDigest(
|
|
106
|
+
fact: PublicationFailureFact,
|
|
107
|
+
baselineReadback: ProductionReadback
|
|
108
|
+
): string;
|
|
104
109
|
export class ReleaseControlProducer {
|
|
105
110
|
constructor(options: {
|
|
106
111
|
installationKey: string;
|
package/dist/producer.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { sameIdentity, acceptedProduction } from './protocol.mjs';
|
|
|
3
3
|
import { blocker, failureMessage, failedProduction } from './producer-support.mjs';
|
|
4
4
|
import { ProducerExecution } from './producer-execution.mjs';
|
|
5
5
|
import { reconcilePublicationFailure } from './producer-publication-failure.mjs';
|
|
6
|
+
export { publicationFailureProofDigest } from './producer-publication-failure.mjs';
|
|
6
7
|
export {
|
|
7
8
|
sameIdentity,
|
|
8
9
|
validateExpectedSource,
|
package/dist/provenance.json
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"package": "@casecore/release-agent",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"sourceCommit": "
|
|
4
|
+
"version": "0.1.2",
|
|
5
|
+
"sourceCommit": "f51fa3e14522b88887645d3f83cc2dcfaf478d32",
|
|
6
6
|
"sourceVerified": true,
|
|
7
7
|
"sourceDirty": false,
|
|
8
8
|
"buildKind": "release",
|
|
9
|
-
"contentDigest": "sha256:
|
|
9
|
+
"contentDigest": "sha256:8b324e7f25b018f0b79a9deded8aada1b885646a807578a2697bd47b7c8fb5a0",
|
|
10
10
|
"files": {
|
|
11
11
|
"src/configuration-hook.mjs": "sha256:1171bf982cbf7cb0092f63b633ef0b0a0947d8ed1106f88c9175c8add33d81b1",
|
|
12
12
|
"src/exclusive-operation.mjs": "sha256:5a1d0b7d3aa41b901132e239586d66d6c7aa2c16682a7db033a7aab91b1d781a",
|
|
13
13
|
"src/identity.mjs": "sha256:253021058fc79232d83f3ecba6f1023fd046f27aabdf9c129a103d3c9ccb07f5",
|
|
14
|
-
"src/index.mjs": "sha256:
|
|
14
|
+
"src/index.mjs": "sha256:76bda5e1dc376bb7033d8fdbddb0afa85c7c68bbe028aea078ffceaaa35ec75e",
|
|
15
15
|
"src/journal.mjs": "sha256:35a2620d56084ec1aa164e09d77b8e77096413d9572f7b601da8ef7149b996bd",
|
|
16
16
|
"src/pipeline.mjs": "sha256:7a2d39d235ce7160441d177200389c5f3eae1cc494ec36082a23e8d64a0abe3f",
|
|
17
17
|
"src/producer-admission.mjs": "sha256:975c7dac25d4c8f94423e5a4db2bd99c556afd6492b26fbda58e841f1df8fc3c",
|
|
18
18
|
"src/producer-delivery.mjs": "sha256:b20d9ae20dee90bbbe739245a84c9f4b91b1ab958b5e2995ff8705da24ba3988",
|
|
19
19
|
"src/producer-execution.mjs": "sha256:6d9fbca493e4386c92c4d853a66ece3677ec7ab644d6da15b68dd744f30186ef",
|
|
20
|
-
"src/producer-journal.mjs": "sha256:
|
|
21
|
-
"src/producer-publication-failure.mjs": "sha256:
|
|
20
|
+
"src/producer-journal.mjs": "sha256:0934057702fb611b9a8d9dde58cb2c2024f436a49a4e5f6ab02f8884a320cea7",
|
|
21
|
+
"src/producer-publication-failure.mjs": "sha256:726b2b85880143b70219473896b894f64b25b1c495f77488f1e73ce950b839f6",
|
|
22
22
|
"src/producer-publication.mjs": "sha256:c09fa07b37507093b71bbaff3af9df8ec2331c0cba24a4b936d247e9b35257f7",
|
|
23
23
|
"src/producer-server.mjs": "sha256:def28e10a17eb80fb04ba69b09050731bb005949f949a7f1fb46dae267323434",
|
|
24
24
|
"src/producer-state.mjs": "sha256:c3fcdace4c6579be092d39803cfb308e4bf88060b3f75bb7fc8b91284609b4ce",
|
|
25
25
|
"src/producer-support.mjs": "sha256:0a10f6fa50cd8e729ba432640d0845004488ead2bcc1c9383f02548037f83d66",
|
|
26
|
-
"src/producer.mjs": "sha256:
|
|
26
|
+
"src/producer.mjs": "sha256:6d0b875a9901a14edf5b93c771356f5352e924ff7977c13b027343c9bdca42f4",
|
|
27
27
|
"src/protocol-internals.mjs": "sha256:d08a0c72079d091792c5327adafac5ffc38ea20138a6c44b88879e7157a890b3",
|
|
28
28
|
"src/protocol.mjs": "sha256:1fe135a2e60922b517886a392c2b0c2250b773abda8b1a1fcd9af86df2be6dae",
|
|
29
29
|
"src/snapshot-validator.mjs": "sha256:1279aa19e0932776bc1998935f70ca6fae65391f96b60e2d65bcab3b2cb46036",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"types/index.d.ts": "sha256:92ef7b5600f05d52dcb646fc3e4d2b4944ead52ff1c9ee23bb9f4659bebf05a6",
|
|
34
34
|
"types/journal.d.ts": "sha256:79beb61af50b00400588a87426a7f5b38bf38d4b8224284dc6e0631378aa244e",
|
|
35
35
|
"types/pipeline.d.ts": "sha256:c9135e25fb852377c32a16ce781405393c5857886e3f77754b715747202c70a9",
|
|
36
|
-
"types/producer.d.ts": "sha256:
|
|
36
|
+
"types/producer.d.ts": "sha256:980aad137365f6c131b6bfabfab411a73c4a2b2fb35a167152ef8ed446ec1ade",
|
|
37
37
|
"types/protocol.d.ts": "sha256:02c06986c81bfc4214169f62da8e0566704b71a5add58434dfca92c673fad4ce",
|
|
38
38
|
"types/source-store.d.ts": "sha256:de044c2bca9826b6c20caefb00d22a4c78183a869b8835fb8f15935989e2372a",
|
|
39
39
|
"types/types.d.ts": "sha256:cdc6664854bffc371a3da45f29bf1b31f14fd61b66e8f8bae8309dd836f6de6e"
|