@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 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
- `proofDigest` имеет вид `sha256:<hex>` и вычисляется движком из `fact`.
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,4 +1,5 @@
1
1
  export * from './pipeline.mjs';
2
2
  export * from './source-store.mjs';
3
3
  export * from './producer.mjs';
4
+ export { publicationFailureProofDigest } from './producer-publication-failure.mjs';
4
5
  export { createProtectedJsonStore } from './journal.mjs';
@@ -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 { validatePublicationFailureRecord } from './producer-publication-failure.mjs';
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 { canonical, hash, keys, timestamp } from './protocol-internals.mjs';
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 validatePublicationFailureRecord(record, command, delivery) {
48
- keys(record, ['state', 'fact']);
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 ??= { state: 'pending', fact: null };
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 age = producer.now().getTime() - Date.parse(fact.observedAt);
117
- assert.ok(age >= -30000 && age <= 30000, 'Fresh publication failure authority required');
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 = `sha256:${hash(canonical(fact))}`;
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 = producer.now().toISOString();
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',
@@ -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,
@@ -1,29 +1,29 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "package": "@casecore/release-agent",
4
- "version": "0.1.1",
5
- "sourceCommit": "afb036322db3a5ff3bbe4d3d2d975837ebf469b1",
4
+ "version": "0.1.2",
5
+ "sourceCommit": "f51fa3e14522b88887645d3f83cc2dcfaf478d32",
6
6
  "sourceVerified": true,
7
7
  "sourceDirty": false,
8
8
  "buildKind": "release",
9
- "contentDigest": "sha256:c5f5749d35f85bf2ae7878983ecfd0ef77ee065e1526bf29d54680ddf1b9cc70",
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:38d3ebcffe8716f7146c18061d26a392d7784b97a79c59ed53ff839175de2f94",
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:4799a2224ff1004d68892b807b222da708f8f51e0554b01f54d5c877a8ef84bd",
21
- "src/producer-publication-failure.mjs": "sha256:513e71276c7ce4088c95cb3605917afc4709cbb7222ac746958923d7f18eac70",
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:7dd4e6118531c25c6d5547d769263858e556731c76af87fba4c21c8ed98b3994",
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:16ec1e81964a982c9653cdfe70eb3aa22ea8dd7200c2949a8e6731686df7a7bb",
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casecore/release-agent",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Общий движок поставки CaseCore с адаптерами продуктов",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {