@casecore/release-agent 0.1.0 → 0.1.1

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
@@ -49,6 +49,43 @@ import { ReleaseControlProducer } from '@casecore/release-agent/producer';
49
49
  `production_acceptance` подтверждается через независимый `readProduction`.
50
50
  Журнал сохраняет прежний формат MyRobotica и исходные идентификаторы команд.
51
51
 
52
+ UUID исходной попытки доставки и UUID команды CaseCore могут различаться.
53
+ `provider.verifyPublicationDelivery(command, delivery)` подтверждает их связь
54
+ по каноническому источнику продукта до `bindPublicationDelivery`: исходный commit,
55
+ репозиторий, ветка, выбранный исполнитель, доверенный статус, T0 и deadline
56
+ сохраняются. Для разных UUID обработчик обязан вернуть `true`; при подключённом
57
+ обработчике он проверяет и совпадающие UUID. При его отсутствии поддерживается
58
+ прежний контракт с совпадающими UUID.
59
+
60
+ После ошибки публикации до появления артефакта существующий
61
+ `reconcile(commandId)` вызывает подключённый
62
+ `provider.reconcilePublicationFailure(command, { signal, delivery, recoveryDeadlineAt })`.
63
+ Адаптер возвращает `null` при неопределённом результате либо удерживаемый guard:
64
+ `{ fact, assertOwned(), release({ resolved, proofDigest }) }`.
65
+ `fact` содержит ровно `schema: 1`, `kind: 'failed_before_installation'`,
66
+ `commandId`, `installationKey`, `expectedSource`, исходный `delivery`, свежий
67
+ `observedAt`, `stage: source_ready | ci | build | verify | publish`,
68
+ `authenticated: true`, `executionStopped: true`, `pendingTransaction: false`,
69
+ `publicationResolved: true`, `installationNotStarted: true`.
70
+
71
+ Адаптер подтверждает фактическую остановку исполнителя и его дочерних/удалённых
72
+ операций, исходную идентичность и отсутствие незавершённой установки через
73
+ существующие журналы и механизмы владения продукта. Он удерживает исключение
74
+ конкурирующих действий до сохранения pending-доказательства и подтверждения
75
+ его durable-завершения в `release({ resolved: true, proofDigest })`.
76
+ `proofDigest` имеет вид `sha256:<hex>` и вычисляется движком из `fact`.
77
+ Ошибка/потеря ACK не разрешает следующий выпуск. Guard и его callback работают
78
+ внутри доверенного адаптера; HTTP-запрос не может подставить этот результат.
79
+
80
+ Только после подтверждённого завершения движок сохраняет `failed` и
81
+ `recoveryState: verified`, оставляя исходную команду, часы, диагностику и успешные
82
+ этапы. Артефакт и успешная установка не подставляются. Сохранённый pending
83
+ запрещает команды, продолжение и резервирование после перезапуска; восстановление
84
+ проходит через тот же `reconcile` и новую фактическую проверку владения.
85
+ Его отдельный предел — 30 секунд; срок публикации не продлевается.
86
+ Обычный `reserveExclusiveOperation` становится доступен после завершения
87
+ восстановления, когда остальные операции установки также разрешены.
88
+
52
89
  Источник публикации ограничивается установкой. Для `main` передаётся точная
53
90
  политика, например `sourcePolicy: { repository: 'owner/product', ref: 'main' }`.
54
91
  Без политики сохраняется прежнее ограничение MyRobotica на ветки `codex/`.
@@ -3,13 +3,27 @@ import assert from 'node:assert/strict';
3
3
  // Generic process-local ownership only. Products restore pending ownership from
4
4
  // their own durable configuration state before exposing effect-bearing methods.
5
5
  const operations = new WeakMap();
6
- const mutableMethods = ['command', 'execute', 'resumeScheduled', 'resumePublication',
7
- 'reconcile', 'bindPublicationDelivery', 'bindPublicationArtifact', 'qualifyForwardRecovery'];
6
+ const mutableMethods = [
7
+ 'command',
8
+ 'execute',
9
+ 'resumeScheduled',
10
+ 'resumePublication',
11
+ 'reconcile',
12
+ 'bindPublicationDelivery',
13
+ 'bindPublicationArtifact',
14
+ 'qualifyForwardRecovery',
15
+ ];
8
16
  function metadata(producer) {
9
17
  const state = operations.get(producer);
10
18
  assert.ok(state, 'DELIVERY_OPERATION_BARRIER_REQUIRED');
11
19
  return state;
12
20
  }
21
+ function pendingFailures(producer, state) {
22
+ return [
23
+ ...Object.values(producer.state.commands),
24
+ ...(state.loaded ? [] : Object.values(producer.journal.read().commands)),
25
+ ].filter((item) => item.publicationFailureReconciliation?.state === 'pending');
26
+ }
13
27
  export function installOperationBarrier(producer) {
14
28
  assert.ok(!operations.has(producer), 'DELIVERY_OPERATION_BARRIER_ALREADY_INSTALLED');
15
29
  operations.set(producer, { activity: 0, lease: null, loaded: false });
@@ -19,10 +33,26 @@ export function installOperationBarrier(producer) {
19
33
  Object.defineProperty(producer, name, {
20
34
  value: async function (...args) {
21
35
  const state = metadata(this);
22
- assert.ok(!state.lease, 'DELIVERY_EXCLUSIVE_OPERATION_HELD');
36
+ assert.ok(
37
+ !state.lease ||
38
+ (name === 'reconcile' &&
39
+ state.lease.kind === 'publicationFailure' &&
40
+ state.lease.status === 'held' &&
41
+ state.lease.commandId === args[0]),
42
+ 'DELIVERY_EXCLUSIVE_OPERATION_HELD'
43
+ );
44
+ assert.ok(
45
+ pendingFailures(this, state).every(
46
+ (item) => name === 'reconcile' && item.command.commandId === args[0]
47
+ ),
48
+ 'DELIVERY_PUBLICATION_FAILURE_RECONCILIATION_PENDING'
49
+ );
23
50
  state.activity++;
24
- try { return await method.apply(this, args); }
25
- finally { state.activity--; }
51
+ try {
52
+ return await method.apply(this, args);
53
+ } finally {
54
+ state.activity--;
55
+ }
26
56
  },
27
57
  });
28
58
  }
@@ -33,34 +63,110 @@ export function markOperationStateLoaded(producer) {
33
63
  function installationBusy(producer, state) {
34
64
  // A restored configuration reservation precedes initialize. Inspect the
35
65
  // protected existing journal so an empty constructor cannot hide a live run.
36
- const runs = state.loaded ? producer.state.runs :
37
- [...producer.state.runs, ...producer.journal.read().runs];
38
- return state.activity > 0 || producer.admitting !== undefined ||
39
- producer.pending.size > 0 || producer.scheduling === true ||
40
- runs.some((run) =>
41
- ['queued', 'running'].includes(run.status) ||
42
- (run.status === 'blocked' && (run.recoveryState !== 'verified' ||
43
- run.blockers?.some((reason) => reason.code === 'RECONCILIATION_REQUIRED'))) ||
44
- ['failed', 'unverified'].includes(run.recoveryState));
66
+ const runs = state.loaded
67
+ ? producer.state.runs
68
+ : [...producer.state.runs, ...producer.journal.read().runs];
69
+ return (
70
+ pendingFailures(producer, state).length > 0 ||
71
+ state.activity > 0 ||
72
+ producer.admitting !== undefined ||
73
+ producer.pending.size > 0 ||
74
+ producer.scheduling === true ||
75
+ runs.some(
76
+ (run) =>
77
+ ['queued', 'running'].includes(run.status) ||
78
+ (run.status === 'blocked' &&
79
+ (run.recoveryState !== 'verified' ||
80
+ run.blockers?.some((reason) => reason.code === 'RECONCILIATION_REQUIRED'))) ||
81
+ ['failed', 'unverified'].includes(run.recoveryState)
82
+ )
83
+ );
84
+ }
85
+
86
+ // Only the existing reconcile entrypoint can recover its own pre-artifact run.
87
+ // General configuration reservations retain the unchanged busy-run refusal.
88
+ export function reservePublicationFailureReconciliation(producer, commandId) {
89
+ const state = metadata(producer);
90
+ assert.ok(
91
+ state.loaded &&
92
+ state.activity === 1 &&
93
+ producer.admitting === undefined &&
94
+ producer.pending.size === 0 &&
95
+ producer.scheduling !== true,
96
+ 'DELIVERY_INSTALLATION_BUSY'
97
+ );
98
+ assert.ok(
99
+ !state.lease ||
100
+ (state.lease.kind === 'publicationFailure' &&
101
+ state.lease.status === 'held' &&
102
+ state.lease.commandId === commandId),
103
+ 'DELIVERY_EXCLUSIVE_OPERATION_HELD'
104
+ );
105
+ assert.ok(
106
+ !producer.state.runs.some(
107
+ (run) =>
108
+ run.commandId !== commandId &&
109
+ (['queued', 'running'].includes(run.status) ||
110
+ (run.status === 'blocked' &&
111
+ (run.recoveryState !== 'verified' ||
112
+ run.blockers?.some((reason) => reason.code === 'RECONCILIATION_REQUIRED'))) ||
113
+ ['failed', 'unverified'].includes(run.recoveryState))
114
+ ),
115
+ 'DELIVERY_INSTALLATION_BUSY'
116
+ );
117
+ assert.ok(
118
+ !Object.values(producer.state.commands).some(
119
+ (item) =>
120
+ item.command.commandId !== commandId &&
121
+ item.publicationFailureReconciliation?.state === 'pending'
122
+ ),
123
+ 'DELIVERY_INSTALLATION_BUSY'
124
+ );
125
+ const generation = Symbol();
126
+ state.lease = { kind: 'publicationFailure', commandId, generation, status: 'active' };
127
+ const assertOwned = () =>
128
+ assert.ok(
129
+ state.lease?.generation === generation && state.lease.status === 'active',
130
+ 'DELIVERY_EXCLUSIVE_OPERATION_NOT_OWNED'
131
+ );
132
+ return {
133
+ assertOwned,
134
+ release({ resolved }) {
135
+ assertOwned();
136
+ if (resolved === true) state.lease = null;
137
+ else state.lease.status = 'held';
138
+ },
139
+ };
45
140
  }
46
141
  export function reserveExclusiveOperation(producer, { operationId } = {}) {
47
- assert.ok(typeof operationId === 'string' && operationId.trim().length > 0 && operationId.length <= 200,
48
- 'DELIVERY_OPERATION_ID_REQUIRED');
142
+ assert.ok(
143
+ typeof operationId === 'string' && operationId.trim().length > 0 && operationId.length <= 200,
144
+ 'DELIVERY_OPERATION_ID_REQUIRED'
145
+ );
49
146
  const state = metadata(producer);
50
- assert.ok(!state.lease || (state.lease.status === 'held' && state.lease.operationId === operationId),
51
- 'DELIVERY_EXCLUSIVE_OPERATION_HELD');
147
+ assert.ok(
148
+ !state.lease || (state.lease.status === 'held' && state.lease.operationId === operationId),
149
+ 'DELIVERY_EXCLUSIVE_OPERATION_HELD'
150
+ );
52
151
  assert.ok(!installationBusy(producer, state), 'DELIVERY_INSTALLATION_BUSY');
53
152
  const generation = Symbol();
54
153
  state.lease = { operationId, generation, status: 'active' };
55
- const assertOwned = () => assert.ok(state.lease?.generation === generation &&
56
- state.lease.status === 'active', 'DELIVERY_EXCLUSIVE_OPERATION_NOT_OWNED');
154
+ const assertOwned = () =>
155
+ assert.ok(
156
+ state.lease?.generation === generation && state.lease.status === 'active',
157
+ 'DELIVERY_EXCLUSIVE_OPERATION_NOT_OWNED'
158
+ );
57
159
  return {
58
160
  assertOwned,
59
161
  release(options = {}) {
60
162
  assertOwned();
61
- assert.ok(options && typeof options === 'object' && !Array.isArray(options) &&
62
- (options.resolved === undefined || typeof options.resolved === 'boolean'),
63
- 'DELIVERY_OPERATION_RESOLUTION_INVALID');
163
+ assert.ok(
164
+ options &&
165
+ typeof options === 'object' &&
166
+ !Array.isArray(options) &&
167
+ (options.resolved === undefined || typeof options.resolved === 'boolean'),
168
+ 'DELIVERY_OPERATION_RESOLUTION_INVALID'
169
+ );
64
170
  if (options?.resolved === true) state.lease = null;
65
171
  else state.lease.status = 'held';
66
172
  },
@@ -15,14 +15,10 @@ export class ProducerAdmission extends ProducerState {
15
15
  assert.equal(
16
16
  existing.fingerprint,
17
17
  fingerprint,
18
- 'Command ID already belongs to different input',
18
+ 'Command ID already belongs to different input'
19
19
  );
20
20
  if (preparedRunId !== undefined)
21
- assert.equal(
22
- existing.ack.runId,
23
- preparedRunId,
24
- 'Prepared run ID changed',
25
- );
21
+ assert.equal(existing.ack.runId, preparedRunId, 'Prepared run ID changed');
26
22
  return {
27
23
  ...existing.ack,
28
24
  state: existing.ack.state === 'rejected' ? 'rejected' : 'reconciled',
@@ -36,9 +32,7 @@ export class ProducerAdmission extends ProducerState {
36
32
  const current = await this.snapshot('0', {
37
33
  deadlineAt: input.deadlineAt,
38
34
  });
39
- const candidate = current.candidates.find(
40
- (c) => c.id === input.candidateId,
41
- );
35
+ const candidate = current.candidates.find((c) => c.id === input.candidateId);
42
36
  const publication = input.action === 'publish';
43
37
  let reason = null;
44
38
  if (
@@ -48,28 +42,21 @@ export class ProducerAdmission extends ProducerState {
48
42
  candidate.state !== 'ready' ||
49
43
  !sameIdentity(candidate.identity, input.expectedIdentity))
50
44
  )
51
- reason = blocker(
52
- 'CANDIDATE_CHANGED',
53
- 'Кандидат изменился или ещё не готов',
54
- );
45
+ reason = blocker('CANDIDATE_CHANGED', 'Кандидат изменился или ещё не готов');
55
46
  if (
56
47
  publication &&
57
48
  (!candidate ||
58
49
  candidate.state !== 'source_ready' ||
59
50
  !candidate.expectedSource ||
60
- canonical(candidate.expectedSource) !==
61
- canonical(input.expectedSource) ||
62
- !Object.values(candidate.identity ?? {}).every(
63
- (item) => item === null,
64
- ) ||
51
+ canonical(candidate.expectedSource) !== canonical(input.expectedSource) ||
52
+ !Object.values(candidate.identity ?? {}).every((item) => item === null) ||
65
53
  Object.keys(candidate.identity ?? {})
66
54
  .sort()
67
- .join(',') !==
68
- 'configDigest,imageDigest,schemaVersion,sourceCommit,version')
55
+ .join(',') !== 'configDigest,imageDigest,schemaVersion,sourceCommit,version')
69
56
  )
70
57
  reason = blocker(
71
58
  'SOURCE_CANDIDATE_CHANGED',
72
- 'Исходный кандидат изменился или ещё не готов',
59
+ 'Исходный кандидат изменился или ещё не готов'
73
60
  );
74
61
  if (
75
62
  input.action !== 'rollback' &&
@@ -79,23 +66,15 @@ export class ProducerAdmission extends ProducerState {
79
66
  (publication
80
67
  ? input.expectedSource.sourceCommit
81
68
  : input.expectedIdentity.sourceCommit) ||
82
- candidate.changes.baseCommit !==
83
- current.installed.identity.sourceCommit)
69
+ candidate.changes.baseCommit !== current.installed.identity.sourceCommit)
84
70
  )
85
71
  reason = blocker(
86
72
  'SOURCE_COMPARISON_UNVERIFIED',
87
- 'Точный состав изменений относительно установленной версии не подтверждён',
73
+ 'Точный состав изменений относительно установленной версии не подтверждён'
88
74
  );
89
75
  if (Date.parse(input.requestedAt) > this.now().getTime() + 30000)
90
- reason = blocker(
91
- 'INVALID_REQUEST_TIME',
92
- 'Время исходного запроса находится в будущем',
93
- );
94
- if (
95
- !reason &&
96
- input.action !== 'rollback' &&
97
- this.provider.validateAdmission
98
- ) {
76
+ reason = blocker('INVALID_REQUEST_TIME', 'Время исходного запроса находится в будущем');
77
+ if (!reason && input.action !== 'rollback' && this.provider.validateAdmission) {
99
78
  try {
100
79
  await this.provider.validateAdmission(input, candidate, {
101
80
  now: this.now(),
@@ -103,18 +82,21 @@ export class ProducerAdmission extends ProducerState {
103
82
  } catch {
104
83
  reason = blocker(
105
84
  'DELIVERY_EXECUTION_INVALID',
106
- 'Исходный срок доставки не подтверждён или изменён',
85
+ 'Исходный срок доставки не подтверждён или изменён'
107
86
  );
108
87
  }
109
88
  }
110
89
  if (input.lane === 'scheduled' && !this.provider.announce)
111
90
  reason = blocker(
112
91
  'UNSUPPORTED_SCHEDULE',
113
- 'Транспорт уведомления об окне установки не подключён',
92
+ 'Транспорт уведомления об окне установки не подключён'
114
93
  );
115
94
  if (Date.parse(input.deadlineAt) <= this.now().getTime())
116
95
  reason = blocker('DEADLINE_EXCEEDED', 'Исходный срок выполнения истёк');
117
96
  if (
97
+ Object.values(this.state.commands).some(
98
+ (item) => item.publicationFailureReconciliation?.state === 'pending'
99
+ ) ||
118
100
  this.state.runs.some(
119
101
  (r) =>
120
102
  ['queued', 'running'].includes(r.status) ||
@@ -126,44 +108,34 @@ export class ProducerAdmission extends ProducerState {
126
108
  input.action === 'rollback' &&
127
109
  r.id === input.previousRunId &&
128
110
  ['failed', 'blocked'].includes(r.status)
129
- )),
111
+ ))
130
112
  )
131
113
  )
132
114
  reason = blocker(
133
115
  'INSTALLATION_BUSY',
134
116
  'Установка уже занята или требует восстановления',
135
- true,
117
+ true
136
118
  );
137
119
  if (!this.provider.actions?.includes(input.action))
138
- reason = blocker(
139
- 'UNSUPPORTED_ACTION',
140
- 'Операция не подключена к исполнительному агенту',
141
- );
120
+ reason = blocker('UNSUPPORTED_ACTION', 'Операция не подключена к исполнительному агенту');
142
121
  if (!['deploy', 'publish'].includes(input.action)) {
143
- const previous = this.state.runs.find(
144
- (r) => r.id === input.previousRunId,
145
- );
146
- const previousItem =
147
- previous && this.state.commands[previous.commandId];
122
+ const previous = this.state.runs.find((r) => r.id === input.previousRunId);
123
+ const previousItem = previous && this.state.commands[previous.commandId];
148
124
  const previousCommand = previousItem?.command;
149
125
  if (
150
126
  !previous ||
151
127
  !previousCommand ||
152
128
  (input.action === 'retry'
153
129
  ? previousCommand.candidateId !== input.candidateId
154
- : !sameIdentity(
155
- previousItem.baseline?.identity,
156
- input.expectedIdentity,
157
- )) ||
130
+ : !sameIdentity(previousItem.baseline?.identity, input.expectedIdentity)) ||
158
131
  (input.action === 'retry' &&
159
132
  (previousCommand.deadlineAt !== input.deadlineAt ||
160
133
  previousCommand.requestedAt !== input.requestedAt)) ||
161
- (input.action === 'rollback' &&
162
- !['failed', 'blocked'].includes(previous.status))
134
+ (input.action === 'rollback' && !['failed', 'blocked'].includes(previous.status))
163
135
  )
164
136
  reason = blocker(
165
137
  'INVALID_CONTINUATION',
166
- 'Продолжение должно сохранить исходный кандидат и срок',
138
+ 'Продолжение должно сохранить исходный кандидат и срок'
167
139
  );
168
140
  }
169
141
  const runId = preparedRunId ?? input.commandId;
@@ -178,12 +150,8 @@ export class ProducerAdmission extends ProducerState {
178
150
  fingerprint,
179
151
  command: structuredClone(input),
180
152
  baseline: structuredClone(current.installed),
181
- ...(publication && candidate
182
- ? { sourceCandidate: structuredClone(candidate) }
183
- : {}),
184
- ...(candidate?.deliveryExecution
185
- ? { deliveryExecution: candidate.deliveryExecution }
186
- : {}),
153
+ ...(publication && candidate ? { sourceCandidate: structuredClone(candidate) } : {}),
154
+ ...(candidate?.deliveryExecution ? { deliveryExecution: candidate.deliveryExecution } : {}),
187
155
  ack,
188
156
  };
189
157
  if (!reason) {
@@ -220,7 +188,7 @@ export class ProducerAdmission extends ProducerState {
220
188
  null,
221
189
  'command',
222
190
  reason ? reason.message : 'Команда принята',
223
- reason ? 'warning' : 'info',
191
+ reason ? 'warning' : 'info'
224
192
  );
225
193
  await this.save();
226
194
  if (!reason) {
@@ -0,0 +1,34 @@
1
+ import assert from 'node:assert/strict';
2
+ import { hash, canonical } from './protocol-internals.mjs';
3
+ import { validateOriginalDelivery } from './producer-support.mjs';
4
+
5
+ export async function verifyPublicationDelivery(
6
+ producer,
7
+ command,
8
+ delivery,
9
+ { fresh = true } = {}
10
+ ) {
11
+ assert.equal(command.action, 'publish');
12
+ validateOriginalDelivery(delivery, command.expectedSource.sourceCommit);
13
+ assert.equal(delivery.requestedAt, command.requestedAt);
14
+ assert.equal(delivery.deadlineAt, command.deadlineAt);
15
+ const item = producer.state.commands[command.commandId];
16
+ assert.equal(item?.ack.state, 'accepted', 'Publication command was not admitted');
17
+ assert.equal(item.fingerprint, hash(canonical(command)));
18
+ if (item.deliveryExecution)
19
+ assert.deepEqual(item.deliveryExecution, delivery, 'Original published delivery changed');
20
+ if (typeof producer.provider.verifyPublicationDelivery === 'function')
21
+ assert.equal(
22
+ await producer.provider.verifyPublicationDelivery(command, delivery),
23
+ true,
24
+ 'Original delivery correlation was not verified'
25
+ );
26
+ else
27
+ assert.equal(
28
+ delivery.id,
29
+ command.commandId,
30
+ 'Independent delivery requires a trusted correlation verifier'
31
+ );
32
+ if (fresh) assert.ok(Date.parse(command.deadlineAt) > producer.now().getTime());
33
+ return delivery;
34
+ }
@@ -1,15 +1,22 @@
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
5
 
5
6
  /** Validate persisted state before any provider command can run after restart. */
6
7
  export function validProducerState(state, installationKey, sourcePolicy) {
7
8
  try {
8
- keys(state, ['schema', 'installationKey', 'cursor', 'commands', 'runs', 'events'], ['diagnostics']);
9
+ keys(
10
+ state,
11
+ ['schema', 'installationKey', 'cursor', 'commands', 'runs', 'events'],
12
+ ['diagnostics']
13
+ );
9
14
  assert.equal(state.schema, 1);
10
15
  assert.equal(state.installationKey, installationKey);
11
16
  assert.match(state.cursor, /^\d+$/);
12
- assert.ok(state.commands && typeof state.commands === 'object' && !Array.isArray(state.commands));
17
+ assert.ok(
18
+ state.commands && typeof state.commands === 'object' && !Array.isArray(state.commands)
19
+ );
13
20
  for (const [id, item] of Object.entries(state.commands)) {
14
21
  assert.match(id, uuid);
15
22
  assert.equal(item.command.commandId, id);
@@ -19,16 +26,43 @@ export function validProducerState(state, installationKey, sourcePolicy) {
19
26
  assert.equal(item.ack.protocolVersion, 1);
20
27
  assert.ok(['accepted', 'rejected'].includes(item.ack.state));
21
28
  assert.match(item.ack.runId, uuid);
29
+ if (item.publicationFailureReconciliation !== undefined)
30
+ validatePublicationFailureRecord(
31
+ item.publicationFailureReconciliation,
32
+ item.command,
33
+ item.deliveryExecution
34
+ );
22
35
  }
23
- validateProducerSnapshot({
24
- protocolVersion: 1, installationKey, observedAt: new Date().toISOString(),
25
- connection: 'unverified', installed: null, candidates: [],
26
- runs: state.runs.slice(-100), events: state.events, nextCursor: state.cursor,
27
- }, installationKey, sourcePolicy);
36
+ validateProducerSnapshot(
37
+ {
38
+ protocolVersion: 1,
39
+ installationKey,
40
+ observedAt: new Date().toISOString(),
41
+ connection: 'unverified',
42
+ installed: null,
43
+ candidates: [],
44
+ runs: state.runs.slice(-100),
45
+ events: state.events,
46
+ nextCursor: state.cursor,
47
+ },
48
+ installationKey,
49
+ sourcePolicy
50
+ );
28
51
  for (const run of state.runs) {
29
52
  assert.ok(state.commands[run.commandId]);
30
53
  assert.equal(state.commands[run.commandId].ack.runId, run.id);
54
+ const reconciliation = state.commands[run.commandId].publicationFailureReconciliation;
55
+ if (reconciliation) {
56
+ assert.equal(state.commands[run.commandId].command.action, 'publish');
57
+ assert.ok(!state.commands[run.commandId].artifactIdentity);
58
+ if (reconciliation.state === 'settled') {
59
+ assert.equal(run.status, 'failed');
60
+ assert.equal(run.recoveryState, 'verified');
61
+ } else assert.equal(run.status, 'blocked');
62
+ }
31
63
  }
32
64
  return true;
33
- } catch { return false; }
65
+ } catch {
66
+ return false;
67
+ }
34
68
  }
@@ -0,0 +1,191 @@
1
+ import assert from 'node:assert/strict';
2
+ import { canonical, hash, keys, timestamp } from './protocol-internals.mjs';
3
+ import { validateOriginalDelivery, blocker } from './producer-support.mjs';
4
+ import { verifyPublicationDelivery } from './producer-delivery.mjs';
5
+ import { reservePublicationFailureReconciliation } from './exclusive-operation.mjs';
6
+
7
+ const stages = ['source_ready', 'ci', 'build', 'verify', 'publish'];
8
+ export function validatePublicationFailureFact(fact, command, delivery) {
9
+ keys(fact, [
10
+ 'schema',
11
+ 'kind',
12
+ 'commandId',
13
+ 'installationKey',
14
+ 'expectedSource',
15
+ 'delivery',
16
+ 'observedAt',
17
+ 'stage',
18
+ 'authenticated',
19
+ 'executionStopped',
20
+ 'pendingTransaction',
21
+ 'publicationResolved',
22
+ 'installationNotStarted',
23
+ ]);
24
+ assert.equal(fact.schema, 1);
25
+ assert.equal(fact.kind, 'failed_before_installation');
26
+ assert.equal(fact.commandId, command.commandId);
27
+ assert.equal(fact.installationKey, command.installationKey);
28
+ keys(fact.expectedSource, ['repository', 'ref', 'sourceCommit']);
29
+ assert.equal(canonical(fact.expectedSource), canonical(command.expectedSource));
30
+ validateOriginalDelivery(fact.delivery, command.expectedSource.sourceCommit);
31
+ assert.equal(fact.delivery.requestedAt, command.requestedAt);
32
+ assert.equal(fact.delivery.deadlineAt, command.deadlineAt);
33
+ if (delivery) assert.deepEqual(fact.delivery, delivery);
34
+ timestamp(fact.observedAt);
35
+ assert.ok(stages.includes(fact.stage));
36
+ for (const field of [
37
+ 'authenticated',
38
+ 'executionStopped',
39
+ 'publicationResolved',
40
+ 'installationNotStarted',
41
+ ])
42
+ assert.equal(fact[field], true);
43
+ assert.equal(fact.pendingTransaction, false);
44
+ return fact;
45
+ }
46
+
47
+ export function validatePublicationFailureRecord(record, command, delivery) {
48
+ keys(record, ['state', 'fact']);
49
+ assert.ok(['pending', 'settled'].includes(record.state));
50
+ assert.ok(record.state === 'pending' || record.fact !== null);
51
+ if (record.fact !== null) validatePublicationFailureFact(record.fact, command, delivery);
52
+ }
53
+
54
+ export async function reconcilePublicationFailure(producer, item, run) {
55
+ assert.equal(item.ack.state, 'accepted');
56
+ assert.equal(item.command.action, 'publish');
57
+ assert.equal(run.status, 'blocked');
58
+ assert.ok(!item.artifactIdentity);
59
+ for (const id of ['deploy', 'production_acceptance']) {
60
+ const row = run.stages.find((stage) => stage.id === id);
61
+ assert.ok(
62
+ row && row.startedAt === null && row.status === 'pending',
63
+ 'Installation must reconcile through its existing production boundary'
64
+ );
65
+ }
66
+ const lease = reservePublicationFailureReconciliation(producer, item.command.commandId);
67
+ const controller = new AbortController();
68
+ const recoveryDeadlineAt = producer.now().getTime() + 30000;
69
+ let timer;
70
+ let guard;
71
+ let settled = false;
72
+ let releasing = false;
73
+ const timeout = new Promise((_, reject) => {
74
+ timer = setTimeout(() => {
75
+ controller.abort();
76
+ reject(
77
+ Object.assign(Error('Publication failure reconciliation timed out'), {
78
+ code: 'PUBLICATION_FAILURE_RECONCILIATION_TIMEOUT',
79
+ })
80
+ );
81
+ }, 30000);
82
+ });
83
+ const bounded = (operation) => Promise.race([operation, timeout]);
84
+ const cleanup = (owned) =>
85
+ Promise.resolve()
86
+ .then(() => owned.release({ resolved: false, proofDigest: null }))
87
+ .catch(() => {});
88
+ try {
89
+ item.publicationFailureReconciliation ??= { state: 'pending', fact: null };
90
+ assert.equal(item.publicationFailureReconciliation.state, 'pending');
91
+ await bounded(producer.save());
92
+ const arriving = Promise.resolve().then(() =>
93
+ producer.provider.reconcilePublicationFailure(structuredClone(item.command), {
94
+ signal: controller.signal,
95
+ delivery: structuredClone(item.deliveryExecution ?? null),
96
+ recoveryDeadlineAt,
97
+ })
98
+ );
99
+ // A late native guard still owns its cleanup; timeout never makes it usable.
100
+ arriving.then(
101
+ (late) => {
102
+ if (controller.signal.aborted && late && typeof late.release === 'function')
103
+ void cleanup(late);
104
+ },
105
+ () => {}
106
+ );
107
+ guard = await bounded(arriving);
108
+ if (guard === null) return;
109
+ assert.ok(
110
+ guard && typeof guard.assertOwned === 'function' && typeof guard.release === 'function',
111
+ 'Owned publication failure guard required'
112
+ );
113
+ const fact = structuredClone(
114
+ validatePublicationFailureFact(guard.fact, item.command, item.deliveryExecution)
115
+ );
116
+ const age = producer.now().getTime() - Date.parse(fact.observedAt);
117
+ assert.ok(age >= -30000 && age <= 30000, 'Fresh publication failure authority required');
118
+ const row = run.stages.find((stage) => stage.id === fact.stage);
119
+ assert.ok(
120
+ row && !['passed', 'reused'].includes(row.status),
121
+ 'Failed stage conflicts with retained proof'
122
+ );
123
+ const previous = item.publicationFailureReconciliation.fact;
124
+ if (previous) {
125
+ const { observedAt: _oldTime, ...oldIdentity } = previous;
126
+ const { observedAt: _newTime, ...newIdentity } = fact;
127
+ assert.deepEqual(newIdentity, oldIdentity, 'Publication failure reconciliation changed');
128
+ }
129
+ await bounded(
130
+ verifyPublicationDelivery(producer, item.command, fact.delivery, { fresh: false })
131
+ );
132
+ await bounded(guard.assertOwned());
133
+ lease.assertOwned();
134
+ controller.signal.throwIfAborted();
135
+ item.publicationFailureReconciliation.fact = fact;
136
+ item.deliveryExecution = structuredClone(fact.delivery);
137
+ await bounded(producer.save());
138
+ const proofDigest = `sha256:${hash(canonical(fact))}`;
139
+ await bounded(guard.assertOwned());
140
+ releasing = true;
141
+ await bounded(guard.release({ resolved: true, proofDigest }));
142
+ controller.signal.throwIfAborted();
143
+ lease.assertOwned();
144
+ run.status = 'failed';
145
+ run.recoveryState = 'verified';
146
+ run.completedAt = producer.now().toISOString();
147
+ run.blockers = [
148
+ blocker(
149
+ 'FAILED_BEFORE_INSTALLATION',
150
+ 'Выпуск завершился ошибкой до установки; выполнение остановлено'
151
+ ),
152
+ ];
153
+ if (row.status !== 'failed') {
154
+ row.status = 'failed';
155
+ row.failure = structuredClone(run.blockers[0]);
156
+ row.completedAt = run.completedAt;
157
+ if (row.startedAt)
158
+ row.elapsedMs = Math.max(0, Date.parse(row.completedAt) - Date.parse(row.startedAt));
159
+ }
160
+ item.publicationFailureReconciliation.state = 'settled';
161
+ producer.event(run.id, null, 'failure', run.blockers[0].message, 'error');
162
+ await bounded(producer.save());
163
+ settled = true;
164
+ } catch (error) {
165
+ // Even a completed native ACK cannot authorize admission before local save.
166
+ item.publicationFailureReconciliation ??= { state: 'pending', fact: null };
167
+ item.publicationFailureReconciliation.state = 'pending';
168
+ run.status = 'blocked';
169
+ run.blockers = [
170
+ blocker(
171
+ 'RECONCILIATION_REQUIRED',
172
+ 'Подтверждение остановки выпуска требует восстановления',
173
+ true
174
+ ),
175
+ ];
176
+ producer.diagnostic(run.id, null, error);
177
+ // Persist pending with the same protected store even when a failed queued
178
+ // save poisoned the promise chain; this is recovery, never a force-clear.
179
+ try {
180
+ producer.journal.save(structuredClone(producer.state));
181
+ } catch {
182
+ /* Remain HELD; restart requires journal reconciliation. */
183
+ }
184
+ throw error;
185
+ } finally {
186
+ controller.abort();
187
+ clearTimeout(timer);
188
+ if (guard && !settled && !releasing && typeof guard.release === 'function') void cleanup(guard);
189
+ lease.release({ resolved: settled });
190
+ }
191
+ }
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
- import { hash, canonical, completeIdentity } from './protocol-internals.mjs';
3
- import { validateOriginalDelivery } from './producer-support.mjs';
2
+ import { completeIdentity } from './protocol-internals.mjs';
3
+ import { verifyPublicationDelivery } from './producer-delivery.mjs';
4
4
  import { ProducerAdmission } from './producer-admission.mjs';
5
5
 
6
6
  export class ProducerPublication extends ProducerAdmission {
@@ -9,7 +9,7 @@ export class ProducerPublication extends ProducerAdmission {
9
9
  const item = this.state.commands[command.commandId];
10
10
  assert.ok(
11
11
  completeIdentity(item?.artifactIdentity),
12
- 'Published artifact identity has not been verified',
12
+ 'Published artifact identity has not been verified'
13
13
  );
14
14
  return {
15
15
  ...command,
@@ -17,53 +17,30 @@ export class ProducerPublication extends ProducerAdmission {
17
17
  };
18
18
  }
19
19
  async bindPublicationDelivery(command, delivery) {
20
- assert.equal(command.action, 'publish');
21
- validateOriginalDelivery(delivery, command.expectedSource.sourceCommit);
22
- assert.equal(delivery.id, command.commandId);
23
- assert.equal(delivery.requestedAt, command.requestedAt);
24
- assert.equal(delivery.deadlineAt, command.deadlineAt);
25
- assert.ok(Date.parse(command.deadlineAt) > this.now().getTime());
20
+ await verifyPublicationDelivery(this, command, delivery);
26
21
  const item = this.state.commands[command.commandId];
27
- assert.equal(item.fingerprint, hash(canonical(command)));
28
- if (item.deliveryExecution)
29
- assert.deepEqual(
30
- item.deliveryExecution,
31
- delivery,
32
- 'Original published delivery changed',
33
- );
34
22
  item.deliveryExecution = structuredClone(delivery);
35
23
  await this.save();
36
24
  return delivery;
37
25
  }
38
26
  async bindPublicationArtifact(command, identity, { delivery } = {}) {
39
27
  assert.equal(command.action, 'publish');
40
- assert.ok(
41
- completeIdentity(identity),
42
- 'Published artifact identity must be complete',
43
- );
28
+ assert.ok(completeIdentity(identity), 'Published artifact identity must be complete');
44
29
  await this.bindPublicationDelivery(command, delivery);
45
30
  assert.equal(
46
31
  typeof this.provider.verifyPublicationArtifact,
47
32
  'function',
48
- 'Signed publication lineage verifier is required',
33
+ 'Signed publication lineage verifier is required'
49
34
  );
50
35
  assert.equal(
51
- await this.provider.verifyPublicationArtifact(
52
- command,
53
- identity,
54
- delivery,
55
- ),
36
+ await this.provider.verifyPublicationArtifact(command, identity, delivery),
56
37
  true,
57
- 'Signed publication lineage was not verified',
38
+ 'Signed publication lineage was not verified'
58
39
  );
59
40
  assert.ok(Date.parse(command.deadlineAt) > this.now().getTime());
60
41
  const item = this.state.commands[command.commandId];
61
42
  if (item.artifactIdentity)
62
- assert.deepEqual(
63
- item.artifactIdentity,
64
- identity,
65
- 'Bound artifact identity changed',
66
- );
43
+ assert.deepEqual(item.artifactIdentity, identity, 'Bound artifact identity changed');
67
44
  item.artifactIdentity = structuredClone(identity);
68
45
  await this.save();
69
46
  return identity;
@@ -72,54 +49,47 @@ export class ProducerPublication extends ProducerAdmission {
72
49
  const item = this.state.commands[commandId];
73
50
  assert.ok(
74
51
  item?.ack.state !== 'rejected' && item?.command.action === 'publish',
75
- 'Source publication was not admitted',
52
+ 'Source publication was not admitted'
76
53
  );
77
54
  if ([...this.pending].length) return this.acknowledgement(commandId);
78
55
  assert.ok(
79
56
  Date.parse(item.command.deadlineAt) > this.now().getTime(),
80
- 'Original publication deadline exhausted',
57
+ 'Original publication deadline exhausted'
81
58
  );
82
59
  assert.equal(
83
60
  typeof this.provider.validatePublicationResume,
84
61
  'function',
85
- 'Source publication recovery is not connected',
62
+ 'Source publication recovery is not connected'
86
63
  );
87
64
  assert.equal(
88
65
  await this.provider.validatePublicationResume(item.command, item),
89
66
  true,
90
- 'Source publication recovery is not verified',
91
- );
92
- const run = this.state.runs.find(
93
- (record) => record.commandId === commandId,
67
+ 'Source publication recovery is not verified'
94
68
  );
69
+ const run = this.state.runs.find((record) => record.commandId === commandId);
95
70
  assert.ok(run && !['deployed', 'rolled_back'].includes(run.status));
96
71
  if (run.recoveryState !== 'not_required') {
97
72
  assert.ok(
98
73
  this.provider.remotePublication === true &&
99
74
  typeof this.provider.qualifyPublicationResume === 'function',
100
- 'Pending installation must reconcile before publication resumes',
75
+ 'Pending installation must reconcile before publication resumes'
101
76
  );
102
77
  if (!item.artifactIdentity) {
103
- const prepared = await this.provider.publicationResumeArtifact?.(
104
- item.command,
105
- item,
106
- );
78
+ const prepared = await this.provider.publicationResumeArtifact?.(item.command, item);
107
79
  if (!prepared) return this.acknowledgement(commandId);
108
80
  await this.bindPublicationArtifact(item.command, prepared.identity, {
109
81
  delivery: prepared.delivery,
110
82
  });
111
83
  }
112
84
  if (
113
- (await this.provider.qualifyPublicationResume(
114
- this.resolvedCommand(item.command),
115
- item,
116
- )) !== true
85
+ (await this.provider.qualifyPublicationResume(this.resolvedCommand(item.command), item)) !==
86
+ true
117
87
  )
118
88
  return this.acknowledgement(commandId);
119
89
  }
120
90
  assert.ok(
121
91
  Date.parse(item.command.deadlineAt) > this.now().getTime(),
122
- 'Original publication deadline exhausted',
92
+ 'Original publication deadline exhausted'
123
93
  );
124
94
  run.status = 'queued';
125
95
  run.blockers = [];
@@ -58,6 +58,11 @@ export interface ReleaseProvider {
58
58
  identity: ArtifactIdentity,
59
59
  delivery: OriginalDelivery
60
60
  ): Promise<boolean>;
61
+ verifyPublicationDelivery?(command: RunnerCommand, delivery: OriginalDelivery): Promise<boolean>;
62
+ reconcilePublicationFailure?(
63
+ command: RunnerCommand,
64
+ context: ProviderContext & { delivery: OriginalDelivery | null; recoveryDeadlineAt: number }
65
+ ): Promise<PublicationFailureGuard | null>;
61
66
  validatePublicationResume?(
62
67
  command: RunnerCommand,
63
68
  item: Record<string, unknown>
@@ -76,6 +81,26 @@ export interface ReleaseProvider {
76
81
  ): Promise<Record<string, unknown>>;
77
82
  productionFailure?(fact: ProductionReadback): Error | undefined;
78
83
  }
84
+ export interface PublicationFailureFact {
85
+ schema: 1;
86
+ kind: 'failed_before_installation';
87
+ commandId: string;
88
+ installationKey: string;
89
+ expectedSource: { repository: string; ref: string; sourceCommit: string };
90
+ delivery: OriginalDelivery;
91
+ observedAt: string;
92
+ stage: 'source_ready' | 'ci' | 'build' | 'verify' | 'publish';
93
+ authenticated: true;
94
+ executionStopped: true;
95
+ pendingTransaction: false;
96
+ publicationResolved: true;
97
+ installationNotStarted: true;
98
+ }
99
+ export interface PublicationFailureGuard {
100
+ fact: PublicationFailureFact;
101
+ assertOwned(): Promise<void>;
102
+ release(options: { resolved: boolean; proofDigest: string | null }): Promise<void>;
103
+ }
79
104
  export class ReleaseControlProducer {
80
105
  constructor(options: {
81
106
  installationKey: string;
package/dist/producer.mjs CHANGED
@@ -2,7 +2,14 @@ import assert from 'node:assert/strict';
2
2
  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
- export { sameIdentity, validateExpectedSource, validateRunnerCommand, validateProducerSnapshot, acceptedProduction } from './protocol.mjs';
5
+ import { reconcilePublicationFailure } from './producer-publication-failure.mjs';
6
+ export {
7
+ sameIdentity,
8
+ validateExpectedSource,
9
+ validateRunnerCommand,
10
+ validateProducerSnapshot,
11
+ acceptedProduction,
12
+ } from './protocol.mjs';
6
13
  export { producerServer } from './producer-server.mjs';
7
14
 
8
15
  export class ReleaseControlProducer extends ProducerExecution {
@@ -10,6 +17,17 @@ export class ReleaseControlProducer extends ProducerExecution {
10
17
  const item = this.state.commands[commandId];
11
18
  assert.ok(item, 'Unknown command');
12
19
  const run = this.state.runs.find((r) => r.commandId === commandId);
20
+ if (
21
+ item.command.action === 'publish' &&
22
+ run?.status === 'blocked' &&
23
+ !item.artifactIdentity &&
24
+ typeof this.provider.reconcilePublicationFailure === 'function'
25
+ ) {
26
+ await reconcilePublicationFailure(this, item, run);
27
+ return this.acknowledgement(commandId);
28
+ }
29
+ if (item.publicationFailureReconciliation?.state === 'pending')
30
+ return this.acknowledgement(commandId);
13
31
  if (
14
32
  item.command.action === 'publish' &&
15
33
  run?.status === 'blocked' &&
@@ -41,13 +59,10 @@ export class ReleaseControlProducer extends ProducerExecution {
41
59
  await this.qualifyForwardRecovery(run, item.command);
42
60
  if (run.status === 'failed') {
43
61
  for (const row of run.stages.filter((s) =>
44
- ['running', 'unverified'].includes(s.status),
62
+ ['running', 'unverified'].includes(s.status)
45
63
  )) {
46
64
  row.status = 'failed';
47
- row.failure = blocker(
48
- 'INTERRUPTED_PROOF',
49
- 'Подтверждение результата было прервано',
50
- );
65
+ row.failure = blocker('INTERRUPTED_PROOF', 'Подтверждение результата было прервано');
51
66
  }
52
67
  }
53
68
  await this.save();
@@ -60,7 +75,7 @@ export class ReleaseControlProducer extends ProducerExecution {
60
75
  acceptedProduction(
61
76
  fact,
62
77
  { ...resolved, expectedIdentity: item.baseline.identity },
63
- this.now(),
78
+ this.now()
64
79
  ) &&
65
80
  !sameIdentity(item.baseline.identity, resolved.expectedIdentity)
66
81
  ) {
@@ -71,16 +86,11 @@ export class ReleaseControlProducer extends ProducerExecution {
71
86
  blocker(
72
87
  'RECOVERY_VERIFIED',
73
88
  'Предыдущая установка восстановлена; исходная выкатка не завершена',
74
- true,
89
+ true
75
90
  ),
76
91
  ];
77
92
  run.completedAt = this.now().toISOString();
78
- this.event(
79
- run.id,
80
- null,
81
- 'readback',
82
- 'Подтверждено восстановление предыдущей установки',
83
- );
93
+ this.event(run.id, null, 'readback', 'Подтверждено восстановление предыдущей установки');
84
94
  await this.save();
85
95
  }
86
96
  if (
@@ -90,35 +100,28 @@ export class ReleaseControlProducer extends ProducerExecution {
90
100
  .every((s) => ['passed', 'reused'].includes(s.status))
91
101
  ) {
92
102
  for (const row of run.stages.filter((s) =>
93
- ['deploy', 'production_acceptance'].includes(s.id),
103
+ ['deploy', 'production_acceptance'].includes(s.id)
94
104
  )) {
95
105
  row.status = 'passed';
96
106
  row.completedAt = this.now().toISOString();
97
107
  row.failure = null;
98
108
  if (row.startedAt)
99
- row.elapsedMs = Math.max(
100
- 0,
101
- Date.parse(row.completedAt) - Date.parse(row.startedAt),
102
- );
109
+ row.elapsedMs = Math.max(0, Date.parse(row.completedAt) - Date.parse(row.startedAt));
103
110
  }
104
111
  run.recoveryState = 'verified';
105
112
  run.readback = fact;
106
- run.status =
107
- item.command.action === 'rollback' ? 'rolled_back' : 'deployed';
113
+ run.status = item.command.action === 'rollback' ? 'rolled_back' : 'deployed';
108
114
  run.blockers = [];
109
115
  run.completedAt = this.now().toISOString();
110
116
  this.event(
111
117
  run.id,
112
118
  null,
113
119
  'readback',
114
- 'Фактический результат восстановлен после перезапуска',
120
+ 'Фактический результат восстановлен после перезапуска'
115
121
  );
116
122
  await this.save();
117
123
  }
118
- if (
119
- run.status === 'blocked' &&
120
- failedProduction(fact, resolved, this.now())
121
- ) {
124
+ if (run.status === 'blocked' && failedProduction(fact, resolved, this.now())) {
122
125
  run.readback = fact;
123
126
  const cause = this.provider.productionFailure?.(fact);
124
127
  if (cause) this.diagnostic(run.id, 'production_acceptance', cause);
@@ -130,7 +133,7 @@ export class ReleaseControlProducer extends ProducerExecution {
130
133
  cause?.code || 'FUNCTIONAL_ACCEPTANCE_FAILED',
131
134
  cause
132
135
  ? failureMessage(cause)
133
- : 'Функциональная приёмка установленной версии не пройдена',
136
+ : 'Функциональная приёмка установленной версии не пройдена'
134
137
  );
135
138
  }
136
139
  await this.save();
@@ -1,27 +1,29 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "package": "@casecore/release-agent",
4
- "version": "0.1.0",
5
- "sourceCommit": "fed6c2a396507ac6a488b78d38383593031dfe4a",
4
+ "version": "0.1.1",
5
+ "sourceCommit": "afb036322db3a5ff3bbe4d3d2d975837ebf469b1",
6
6
  "sourceVerified": true,
7
7
  "sourceDirty": false,
8
8
  "buildKind": "release",
9
- "contentDigest": "sha256:1ee3136876107e544ba846d3997fadf9c85ea109950f55a322d3df80eb3c1ea1",
9
+ "contentDigest": "sha256:c5f5749d35f85bf2ae7878983ecfd0ef77ee065e1526bf29d54680ddf1b9cc70",
10
10
  "files": {
11
11
  "src/configuration-hook.mjs": "sha256:1171bf982cbf7cb0092f63b633ef0b0a0947d8ed1106f88c9175c8add33d81b1",
12
- "src/exclusive-operation.mjs": "sha256:05f09115e6292c014f8881d125148983f0097f8b5fa7dbcf365dd8355e747657",
12
+ "src/exclusive-operation.mjs": "sha256:5a1d0b7d3aa41b901132e239586d66d6c7aa2c16682a7db033a7aab91b1d781a",
13
13
  "src/identity.mjs": "sha256:253021058fc79232d83f3ecba6f1023fd046f27aabdf9c129a103d3c9ccb07f5",
14
14
  "src/index.mjs": "sha256:38d3ebcffe8716f7146c18061d26a392d7784b97a79c59ed53ff839175de2f94",
15
15
  "src/journal.mjs": "sha256:35a2620d56084ec1aa164e09d77b8e77096413d9572f7b601da8ef7149b996bd",
16
16
  "src/pipeline.mjs": "sha256:7a2d39d235ce7160441d177200389c5f3eae1cc494ec36082a23e8d64a0abe3f",
17
- "src/producer-admission.mjs": "sha256:61030d58f3a1e66bb487cef60ab17efec364ddfc8c0c61ba7396f64f0ee55de0",
17
+ "src/producer-admission.mjs": "sha256:975c7dac25d4c8f94423e5a4db2bd99c556afd6492b26fbda58e841f1df8fc3c",
18
+ "src/producer-delivery.mjs": "sha256:b20d9ae20dee90bbbe739245a84c9f4b91b1ab958b5e2995ff8705da24ba3988",
18
19
  "src/producer-execution.mjs": "sha256:6d9fbca493e4386c92c4d853a66ece3677ec7ab644d6da15b68dd744f30186ef",
19
- "src/producer-journal.mjs": "sha256:58a31d919300c697dfc3630c856512df5c9c9c47cad4b29c66e4da545a9a0255",
20
- "src/producer-publication.mjs": "sha256:dac520f02205ac6c2b3f66a059f66d8cb61e7b0324e45f9264cf69a96042749b",
20
+ "src/producer-journal.mjs": "sha256:4799a2224ff1004d68892b807b222da708f8f51e0554b01f54d5c877a8ef84bd",
21
+ "src/producer-publication-failure.mjs": "sha256:513e71276c7ce4088c95cb3605917afc4709cbb7222ac746958923d7f18eac70",
22
+ "src/producer-publication.mjs": "sha256:c09fa07b37507093b71bbaff3af9df8ec2331c0cba24a4b936d247e9b35257f7",
21
23
  "src/producer-server.mjs": "sha256:def28e10a17eb80fb04ba69b09050731bb005949f949a7f1fb46dae267323434",
22
24
  "src/producer-state.mjs": "sha256:c3fcdace4c6579be092d39803cfb308e4bf88060b3f75bb7fc8b91284609b4ce",
23
25
  "src/producer-support.mjs": "sha256:0a10f6fa50cd8e729ba432640d0845004488ead2bcc1c9383f02548037f83d66",
24
- "src/producer.mjs": "sha256:24cc6db310bcacf301c28982859d732f07d8c5f468e4b5f172d4eb19f6ca14b9",
26
+ "src/producer.mjs": "sha256:7dd4e6118531c25c6d5547d769263858e556731c76af87fba4c21c8ed98b3994",
25
27
  "src/protocol-internals.mjs": "sha256:d08a0c72079d091792c5327adafac5ffc38ea20138a6c44b88879e7157a890b3",
26
28
  "src/protocol.mjs": "sha256:1fe135a2e60922b517886a392c2b0c2250b773abda8b1a1fcd9af86df2be6dae",
27
29
  "src/snapshot-validator.mjs": "sha256:1279aa19e0932776bc1998935f70ca6fae65391f96b60e2d65bcab3b2cb46036",
@@ -31,7 +33,7 @@
31
33
  "types/index.d.ts": "sha256:92ef7b5600f05d52dcb646fc3e4d2b4944ead52ff1c9ee23bb9f4659bebf05a6",
32
34
  "types/journal.d.ts": "sha256:79beb61af50b00400588a87426a7f5b38bf38d4b8224284dc6e0631378aa244e",
33
35
  "types/pipeline.d.ts": "sha256:c9135e25fb852377c32a16ce781405393c5857886e3f77754b715747202c70a9",
34
- "types/producer.d.ts": "sha256:df99fb3617710eb7bbb7a038df22f089e029f2b1fbd1d82f5ad862dadc3b43da",
36
+ "types/producer.d.ts": "sha256:16ec1e81964a982c9653cdfe70eb3aa22ea8dd7200c2949a8e6731686df7a7bb",
35
37
  "types/protocol.d.ts": "sha256:02c06986c81bfc4214169f62da8e0566704b71a5add58434dfca92c673fad4ce",
36
38
  "types/source-store.d.ts": "sha256:de044c2bca9826b6c20caefb00d22a4c78183a869b8835fb8f15935989e2372a",
37
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.0",
3
+ "version": "0.1.1",
4
4
  "description": "Общий движок поставки CaseCore с адаптерами продуктов",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {