@casecore/release-agent 0.1.0 → 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 +62 -0
- package/dist/exclusive-operation.mjs +129 -23
- package/dist/index.mjs +1 -0
- package/dist/producer-admission.mjs +28 -60
- package/dist/producer-delivery.mjs +34 -0
- package/dist/producer-journal.mjs +60 -8
- package/dist/producer-publication-failure.mjs +270 -0
- package/dist/producer-publication.mjs +19 -49
- package/dist/producer.d.ts +30 -0
- package/dist/producer.mjs +31 -27
- package/dist/provenance.json +12 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,68 @@ 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, baselineReadback, 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
|
+
`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
|
+
|
|
80
|
+
Адаптер подтверждает фактическую остановку исполнителя и его дочерних/удалённых
|
|
81
|
+
операций, исходную идентичность и отсутствие незавершённой установки через
|
|
82
|
+
существующие журналы и механизмы владения продукта. Он удерживает исключение
|
|
83
|
+
конкурирующих действий до сохранения pending-доказательства и подтверждения
|
|
84
|
+
его durable-завершения в `release({ resolved: true, proofDigest })`.
|
|
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
|
+
владение и свежесть перед сохранением завершения.
|
|
100
|
+
Ошибка/потеря ACK не разрешает следующий выпуск. Guard и его callback работают
|
|
101
|
+
внутри доверенного адаптера; HTTP-запрос не может подставить этот результат.
|
|
102
|
+
|
|
103
|
+
Только после подтверждённого завершения движок сохраняет `failed` и
|
|
104
|
+
`recoveryState: verified`, оставляя исходную команду, часы, диагностику и успешные
|
|
105
|
+
этапы. Артефакт и успешная установка не подставляются. Сохранённый pending
|
|
106
|
+
запрещает команды, продолжение и резервирование после перезапуска; восстановление
|
|
107
|
+
проходит через тот же `reconcile` и новую фактическую проверку владения.
|
|
108
|
+
Повторная проверка может обновить время наблюдения, но не исходный baseline.
|
|
109
|
+
Старое завершённое восстановление без readback не превращается в доказанный результат.
|
|
110
|
+
Его отдельный предел — 30 секунд; срок публикации не продлевается.
|
|
111
|
+
Обычный `reserveExclusiveOperation` становится доступен после завершения
|
|
112
|
+
восстановления, когда остальные операции установки также разрешены.
|
|
113
|
+
|
|
52
114
|
Источник публикации ограничивается установкой. Для `main` передаётся точная
|
|
53
115
|
политика, например `sourcePolicy: { repository: 'owner/product', ref: 'main' }`.
|
|
54
116
|
Без политики сохраняется прежнее ограничение 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 = [
|
|
7
|
-
'
|
|
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(
|
|
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 {
|
|
25
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
48
|
-
'
|
|
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(
|
|
51
|
-
'
|
|
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 = () =>
|
|
56
|
-
|
|
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(
|
|
62
|
-
|
|
63
|
-
|
|
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
|
},
|
package/dist/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,25 @@
|
|
|
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 {
|
|
5
|
+
validatePublicationFailureRecord,
|
|
6
|
+
validatePublicationFailureReadback,
|
|
7
|
+
} from './producer-publication-failure.mjs';
|
|
4
8
|
|
|
5
9
|
/** Validate persisted state before any provider command can run after restart. */
|
|
6
10
|
export function validProducerState(state, installationKey, sourcePolicy) {
|
|
7
11
|
try {
|
|
8
|
-
keys(
|
|
12
|
+
keys(
|
|
13
|
+
state,
|
|
14
|
+
['schema', 'installationKey', 'cursor', 'commands', 'runs', 'events'],
|
|
15
|
+
['diagnostics']
|
|
16
|
+
);
|
|
9
17
|
assert.equal(state.schema, 1);
|
|
10
18
|
assert.equal(state.installationKey, installationKey);
|
|
11
19
|
assert.match(state.cursor, /^\d+$/);
|
|
12
|
-
assert.ok(
|
|
20
|
+
assert.ok(
|
|
21
|
+
state.commands && typeof state.commands === 'object' && !Array.isArray(state.commands)
|
|
22
|
+
);
|
|
13
23
|
for (const [id, item] of Object.entries(state.commands)) {
|
|
14
24
|
assert.match(id, uuid);
|
|
15
25
|
assert.equal(item.command.commandId, id);
|
|
@@ -19,16 +29,58 @@ export function validProducerState(state, installationKey, sourcePolicy) {
|
|
|
19
29
|
assert.equal(item.ack.protocolVersion, 1);
|
|
20
30
|
assert.ok(['accepted', 'rejected'].includes(item.ack.state));
|
|
21
31
|
assert.match(item.ack.runId, uuid);
|
|
32
|
+
if (item.publicationFailureReconciliation !== undefined)
|
|
33
|
+
validatePublicationFailureRecord(
|
|
34
|
+
item.publicationFailureReconciliation,
|
|
35
|
+
item.command,
|
|
36
|
+
item.deliveryExecution,
|
|
37
|
+
item.baseline
|
|
38
|
+
);
|
|
22
39
|
}
|
|
23
|
-
validateProducerSnapshot(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
validateProducerSnapshot(
|
|
41
|
+
{
|
|
42
|
+
protocolVersion: 1,
|
|
43
|
+
installationKey,
|
|
44
|
+
observedAt: new Date().toISOString(),
|
|
45
|
+
connection: 'unverified',
|
|
46
|
+
installed: null,
|
|
47
|
+
candidates: [],
|
|
48
|
+
runs: state.runs.slice(-100),
|
|
49
|
+
events: state.events,
|
|
50
|
+
nextCursor: state.cursor,
|
|
51
|
+
},
|
|
52
|
+
installationKey,
|
|
53
|
+
sourcePolicy
|
|
54
|
+
);
|
|
28
55
|
for (const run of state.runs) {
|
|
29
56
|
assert.ok(state.commands[run.commandId]);
|
|
30
57
|
assert.equal(state.commands[run.commandId].ack.runId, run.id);
|
|
58
|
+
const reconciliation = state.commands[run.commandId].publicationFailureReconciliation;
|
|
59
|
+
if (reconciliation) {
|
|
60
|
+
assert.equal(state.commands[run.commandId].command.action, 'publish');
|
|
61
|
+
assert.ok(!state.commands[run.commandId].artifactIdentity);
|
|
62
|
+
if (reconciliation.state === 'settled') {
|
|
63
|
+
assert.equal(run.status, 'failed');
|
|
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
|
+
);
|
|
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
|
+
}
|
|
80
|
+
}
|
|
31
81
|
}
|
|
32
82
|
return true;
|
|
33
|
-
} catch {
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
34
86
|
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
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';
|
|
11
|
+
import { validateOriginalDelivery, blocker } from './producer-support.mjs';
|
|
12
|
+
import { verifyPublicationDelivery } from './producer-delivery.mjs';
|
|
13
|
+
import { reservePublicationFailureReconciliation } from './exclusive-operation.mjs';
|
|
14
|
+
|
|
15
|
+
const stages = ['source_ready', 'ci', 'build', 'verify', 'publish'];
|
|
16
|
+
export function publicationFailureProofDigest(fact, baselineReadback) {
|
|
17
|
+
return `sha256:${hash(canonical({ fact, baselineReadback }))}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validatePublicationFailureFact(fact, command, delivery) {
|
|
21
|
+
keys(fact, [
|
|
22
|
+
'schema',
|
|
23
|
+
'kind',
|
|
24
|
+
'commandId',
|
|
25
|
+
'installationKey',
|
|
26
|
+
'expectedSource',
|
|
27
|
+
'delivery',
|
|
28
|
+
'observedAt',
|
|
29
|
+
'stage',
|
|
30
|
+
'authenticated',
|
|
31
|
+
'executionStopped',
|
|
32
|
+
'pendingTransaction',
|
|
33
|
+
'publicationResolved',
|
|
34
|
+
'installationNotStarted',
|
|
35
|
+
]);
|
|
36
|
+
assert.equal(fact.schema, 1);
|
|
37
|
+
assert.equal(fact.kind, 'failed_before_installation');
|
|
38
|
+
assert.equal(fact.commandId, command.commandId);
|
|
39
|
+
assert.equal(fact.installationKey, command.installationKey);
|
|
40
|
+
keys(fact.expectedSource, ['repository', 'ref', 'sourceCommit']);
|
|
41
|
+
assert.equal(canonical(fact.expectedSource), canonical(command.expectedSource));
|
|
42
|
+
validateOriginalDelivery(fact.delivery, command.expectedSource.sourceCommit);
|
|
43
|
+
assert.equal(fact.delivery.requestedAt, command.requestedAt);
|
|
44
|
+
assert.equal(fact.delivery.deadlineAt, command.deadlineAt);
|
|
45
|
+
if (delivery) assert.deepEqual(fact.delivery, delivery);
|
|
46
|
+
timestamp(fact.observedAt);
|
|
47
|
+
assert.ok(stages.includes(fact.stage));
|
|
48
|
+
for (const field of [
|
|
49
|
+
'authenticated',
|
|
50
|
+
'executionStopped',
|
|
51
|
+
'publicationResolved',
|
|
52
|
+
'installationNotStarted',
|
|
53
|
+
])
|
|
54
|
+
assert.equal(fact[field], true);
|
|
55
|
+
assert.equal(fact.pendingTransaction, false);
|
|
56
|
+
return fact;
|
|
57
|
+
}
|
|
58
|
+
|
|
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']);
|
|
89
|
+
assert.ok(['pending', 'settled'].includes(record.state));
|
|
90
|
+
assert.ok(record.state === 'pending' || record.fact !== null);
|
|
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');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function reconcilePublicationFailure(producer, item, run) {
|
|
101
|
+
assert.equal(item.ack.state, 'accepted');
|
|
102
|
+
assert.equal(item.command.action, 'publish');
|
|
103
|
+
assert.equal(run.status, 'blocked');
|
|
104
|
+
assert.ok(!item.artifactIdentity);
|
|
105
|
+
for (const id of ['deploy', 'production_acceptance']) {
|
|
106
|
+
const row = run.stages.find((stage) => stage.id === id);
|
|
107
|
+
assert.ok(
|
|
108
|
+
row && row.startedAt === null && row.status === 'pending',
|
|
109
|
+
'Installation must reconcile through its existing production boundary'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const lease = reservePublicationFailureReconciliation(producer, item.command.commandId);
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const recoveryDeadlineAt = producer.now().getTime() + 30000;
|
|
115
|
+
let timer;
|
|
116
|
+
let guard;
|
|
117
|
+
let settled = false;
|
|
118
|
+
let releasing = false;
|
|
119
|
+
const timeout = new Promise((_, reject) => {
|
|
120
|
+
timer = setTimeout(() => {
|
|
121
|
+
controller.abort();
|
|
122
|
+
reject(
|
|
123
|
+
Object.assign(Error('Publication failure reconciliation timed out'), {
|
|
124
|
+
code: 'PUBLICATION_FAILURE_RECONCILIATION_TIMEOUT',
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
}, 30000);
|
|
128
|
+
});
|
|
129
|
+
const bounded = (operation) => Promise.race([operation, timeout]);
|
|
130
|
+
const cleanup = (owned) =>
|
|
131
|
+
Promise.resolve()
|
|
132
|
+
.then(() => owned.release({ resolved: false, proofDigest: null }))
|
|
133
|
+
.catch(() => {});
|
|
134
|
+
try {
|
|
135
|
+
item.publicationFailureReconciliation ??= {
|
|
136
|
+
state: 'pending',
|
|
137
|
+
fact: null,
|
|
138
|
+
baselineReadback: null,
|
|
139
|
+
};
|
|
140
|
+
assert.equal(item.publicationFailureReconciliation.state, 'pending');
|
|
141
|
+
run.recoveryState = 'unverified';
|
|
142
|
+
run.completedAt = null;
|
|
143
|
+
await bounded(producer.save());
|
|
144
|
+
const arriving = Promise.resolve().then(() =>
|
|
145
|
+
producer.provider.reconcilePublicationFailure(structuredClone(item.command), {
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
delivery: structuredClone(item.deliveryExecution ?? null),
|
|
148
|
+
recoveryDeadlineAt,
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
// A late native guard still owns its cleanup; timeout never makes it usable.
|
|
152
|
+
arriving.then(
|
|
153
|
+
(late) => {
|
|
154
|
+
if (controller.signal.aborted && late && typeof late.release === 'function')
|
|
155
|
+
void cleanup(late);
|
|
156
|
+
},
|
|
157
|
+
() => {}
|
|
158
|
+
);
|
|
159
|
+
guard = await bounded(arriving);
|
|
160
|
+
if (guard === null) return;
|
|
161
|
+
assert.ok(
|
|
162
|
+
guard && typeof guard.assertOwned === 'function' && typeof guard.release === 'function',
|
|
163
|
+
'Owned publication failure guard required'
|
|
164
|
+
);
|
|
165
|
+
const fact = structuredClone(
|
|
166
|
+
validatePublicationFailureFact(guard.fact, item.command, item.deliveryExecution)
|
|
167
|
+
);
|
|
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();
|
|
188
|
+
const row = run.stages.find((stage) => stage.id === fact.stage);
|
|
189
|
+
assert.ok(
|
|
190
|
+
row && !['passed', 'reused'].includes(row.status),
|
|
191
|
+
'Failed stage conflicts with retained proof'
|
|
192
|
+
);
|
|
193
|
+
const previous = item.publicationFailureReconciliation.fact;
|
|
194
|
+
if (previous) {
|
|
195
|
+
const { observedAt: _oldTime, ...oldIdentity } = previous;
|
|
196
|
+
const { observedAt: _newTime, ...newIdentity } = fact;
|
|
197
|
+
assert.deepEqual(newIdentity, oldIdentity, 'Publication failure reconciliation changed');
|
|
198
|
+
}
|
|
199
|
+
await bounded(
|
|
200
|
+
verifyPublicationDelivery(producer, item.command, fact.delivery, { fresh: false })
|
|
201
|
+
);
|
|
202
|
+
await bounded(guard.assertOwned());
|
|
203
|
+
lease.assertOwned();
|
|
204
|
+
controller.signal.throwIfAborted();
|
|
205
|
+
assertFresh();
|
|
206
|
+
item.publicationFailureReconciliation.fact = fact;
|
|
207
|
+
item.publicationFailureReconciliation.baselineReadback = baselineReadback;
|
|
208
|
+
item.deliveryExecution = structuredClone(fact.delivery);
|
|
209
|
+
run.readback = structuredClone(baselineReadback);
|
|
210
|
+
await bounded(producer.save());
|
|
211
|
+
const proofDigest = publicationFailureProofDigest(fact, baselineReadback);
|
|
212
|
+
await bounded(guard.assertOwned());
|
|
213
|
+
lease.assertOwned();
|
|
214
|
+
controller.signal.throwIfAborted();
|
|
215
|
+
assertFresh();
|
|
216
|
+
releasing = true;
|
|
217
|
+
await bounded(guard.release({ resolved: true, proofDigest }));
|
|
218
|
+
controller.signal.throwIfAborted();
|
|
219
|
+
lease.assertOwned();
|
|
220
|
+
const completedAt = assertFresh().toISOString();
|
|
221
|
+
run.status = 'failed';
|
|
222
|
+
run.recoveryState = 'verified';
|
|
223
|
+
run.completedAt = completedAt;
|
|
224
|
+
run.blockers = [
|
|
225
|
+
blocker(
|
|
226
|
+
'FAILED_BEFORE_INSTALLATION',
|
|
227
|
+
'Выпуск завершился ошибкой до установки; выполнение остановлено'
|
|
228
|
+
),
|
|
229
|
+
];
|
|
230
|
+
if (row.status !== 'failed') {
|
|
231
|
+
row.status = 'failed';
|
|
232
|
+
row.failure = structuredClone(run.blockers[0]);
|
|
233
|
+
row.completedAt = run.completedAt;
|
|
234
|
+
if (row.startedAt)
|
|
235
|
+
row.elapsedMs = Math.max(0, Date.parse(row.completedAt) - Date.parse(row.startedAt));
|
|
236
|
+
}
|
|
237
|
+
item.publicationFailureReconciliation.state = 'settled';
|
|
238
|
+
producer.event(run.id, null, 'failure', run.blockers[0].message, 'error');
|
|
239
|
+
await bounded(producer.save());
|
|
240
|
+
settled = true;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// Even a completed native ACK cannot authorize admission before local save.
|
|
243
|
+
item.publicationFailureReconciliation ??= { state: 'pending', fact: null };
|
|
244
|
+
item.publicationFailureReconciliation.state = 'pending';
|
|
245
|
+
run.status = 'blocked';
|
|
246
|
+
run.recoveryState = 'unverified';
|
|
247
|
+
run.completedAt = null;
|
|
248
|
+
run.blockers = [
|
|
249
|
+
blocker(
|
|
250
|
+
'RECONCILIATION_REQUIRED',
|
|
251
|
+
'Подтверждение остановки выпуска требует восстановления',
|
|
252
|
+
true
|
|
253
|
+
),
|
|
254
|
+
];
|
|
255
|
+
producer.diagnostic(run.id, null, error);
|
|
256
|
+
// Persist pending with the same protected store even when a failed queued
|
|
257
|
+
// save poisoned the promise chain; this is recovery, never a force-clear.
|
|
258
|
+
try {
|
|
259
|
+
producer.journal.save(structuredClone(producer.state));
|
|
260
|
+
} catch {
|
|
261
|
+
/* Remain HELD; restart requires journal reconciliation. */
|
|
262
|
+
}
|
|
263
|
+
throw error;
|
|
264
|
+
} finally {
|
|
265
|
+
controller.abort();
|
|
266
|
+
clearTimeout(timer);
|
|
267
|
+
if (guard && !settled && !releasing && typeof guard.release === 'function') void cleanup(guard);
|
|
268
|
+
lease.release({ resolved: settled });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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 = [];
|
package/dist/producer.d.ts
CHANGED
|
@@ -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,31 @@ 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
|
+
baselineReadback: ProductionReadback;
|
|
102
|
+
assertOwned(): Promise<void>;
|
|
103
|
+
release(options: { resolved: boolean; proofDigest: string | null }): Promise<void>;
|
|
104
|
+
}
|
|
105
|
+
export function publicationFailureProofDigest(
|
|
106
|
+
fact: PublicationFailureFact,
|
|
107
|
+
baselineReadback: ProductionReadback
|
|
108
|
+
): string;
|
|
79
109
|
export class ReleaseControlProducer {
|
|
80
110
|
constructor(options: {
|
|
81
111
|
installationKey: string;
|
package/dist/producer.mjs
CHANGED
|
@@ -2,7 +2,15 @@ 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
|
-
|
|
5
|
+
import { reconcilePublicationFailure } from './producer-publication-failure.mjs';
|
|
6
|
+
export { publicationFailureProofDigest } from './producer-publication-failure.mjs';
|
|
7
|
+
export {
|
|
8
|
+
sameIdentity,
|
|
9
|
+
validateExpectedSource,
|
|
10
|
+
validateRunnerCommand,
|
|
11
|
+
validateProducerSnapshot,
|
|
12
|
+
acceptedProduction,
|
|
13
|
+
} from './protocol.mjs';
|
|
6
14
|
export { producerServer } from './producer-server.mjs';
|
|
7
15
|
|
|
8
16
|
export class ReleaseControlProducer extends ProducerExecution {
|
|
@@ -10,6 +18,17 @@ export class ReleaseControlProducer extends ProducerExecution {
|
|
|
10
18
|
const item = this.state.commands[commandId];
|
|
11
19
|
assert.ok(item, 'Unknown command');
|
|
12
20
|
const run = this.state.runs.find((r) => r.commandId === commandId);
|
|
21
|
+
if (
|
|
22
|
+
item.command.action === 'publish' &&
|
|
23
|
+
run?.status === 'blocked' &&
|
|
24
|
+
!item.artifactIdentity &&
|
|
25
|
+
typeof this.provider.reconcilePublicationFailure === 'function'
|
|
26
|
+
) {
|
|
27
|
+
await reconcilePublicationFailure(this, item, run);
|
|
28
|
+
return this.acknowledgement(commandId);
|
|
29
|
+
}
|
|
30
|
+
if (item.publicationFailureReconciliation?.state === 'pending')
|
|
31
|
+
return this.acknowledgement(commandId);
|
|
13
32
|
if (
|
|
14
33
|
item.command.action === 'publish' &&
|
|
15
34
|
run?.status === 'blocked' &&
|
|
@@ -41,13 +60,10 @@ export class ReleaseControlProducer extends ProducerExecution {
|
|
|
41
60
|
await this.qualifyForwardRecovery(run, item.command);
|
|
42
61
|
if (run.status === 'failed') {
|
|
43
62
|
for (const row of run.stages.filter((s) =>
|
|
44
|
-
['running', 'unverified'].includes(s.status)
|
|
63
|
+
['running', 'unverified'].includes(s.status)
|
|
45
64
|
)) {
|
|
46
65
|
row.status = 'failed';
|
|
47
|
-
row.failure = blocker(
|
|
48
|
-
'INTERRUPTED_PROOF',
|
|
49
|
-
'Подтверждение результата было прервано',
|
|
50
|
-
);
|
|
66
|
+
row.failure = blocker('INTERRUPTED_PROOF', 'Подтверждение результата было прервано');
|
|
51
67
|
}
|
|
52
68
|
}
|
|
53
69
|
await this.save();
|
|
@@ -60,7 +76,7 @@ export class ReleaseControlProducer extends ProducerExecution {
|
|
|
60
76
|
acceptedProduction(
|
|
61
77
|
fact,
|
|
62
78
|
{ ...resolved, expectedIdentity: item.baseline.identity },
|
|
63
|
-
this.now()
|
|
79
|
+
this.now()
|
|
64
80
|
) &&
|
|
65
81
|
!sameIdentity(item.baseline.identity, resolved.expectedIdentity)
|
|
66
82
|
) {
|
|
@@ -71,16 +87,11 @@ export class ReleaseControlProducer extends ProducerExecution {
|
|
|
71
87
|
blocker(
|
|
72
88
|
'RECOVERY_VERIFIED',
|
|
73
89
|
'Предыдущая установка восстановлена; исходная выкатка не завершена',
|
|
74
|
-
true
|
|
90
|
+
true
|
|
75
91
|
),
|
|
76
92
|
];
|
|
77
93
|
run.completedAt = this.now().toISOString();
|
|
78
|
-
this.event(
|
|
79
|
-
run.id,
|
|
80
|
-
null,
|
|
81
|
-
'readback',
|
|
82
|
-
'Подтверждено восстановление предыдущей установки',
|
|
83
|
-
);
|
|
94
|
+
this.event(run.id, null, 'readback', 'Подтверждено восстановление предыдущей установки');
|
|
84
95
|
await this.save();
|
|
85
96
|
}
|
|
86
97
|
if (
|
|
@@ -90,35 +101,28 @@ export class ReleaseControlProducer extends ProducerExecution {
|
|
|
90
101
|
.every((s) => ['passed', 'reused'].includes(s.status))
|
|
91
102
|
) {
|
|
92
103
|
for (const row of run.stages.filter((s) =>
|
|
93
|
-
['deploy', 'production_acceptance'].includes(s.id)
|
|
104
|
+
['deploy', 'production_acceptance'].includes(s.id)
|
|
94
105
|
)) {
|
|
95
106
|
row.status = 'passed';
|
|
96
107
|
row.completedAt = this.now().toISOString();
|
|
97
108
|
row.failure = null;
|
|
98
109
|
if (row.startedAt)
|
|
99
|
-
row.elapsedMs = Math.max(
|
|
100
|
-
0,
|
|
101
|
-
Date.parse(row.completedAt) - Date.parse(row.startedAt),
|
|
102
|
-
);
|
|
110
|
+
row.elapsedMs = Math.max(0, Date.parse(row.completedAt) - Date.parse(row.startedAt));
|
|
103
111
|
}
|
|
104
112
|
run.recoveryState = 'verified';
|
|
105
113
|
run.readback = fact;
|
|
106
|
-
run.status =
|
|
107
|
-
item.command.action === 'rollback' ? 'rolled_back' : 'deployed';
|
|
114
|
+
run.status = item.command.action === 'rollback' ? 'rolled_back' : 'deployed';
|
|
108
115
|
run.blockers = [];
|
|
109
116
|
run.completedAt = this.now().toISOString();
|
|
110
117
|
this.event(
|
|
111
118
|
run.id,
|
|
112
119
|
null,
|
|
113
120
|
'readback',
|
|
114
|
-
'Фактический результат восстановлен после перезапуска'
|
|
121
|
+
'Фактический результат восстановлен после перезапуска'
|
|
115
122
|
);
|
|
116
123
|
await this.save();
|
|
117
124
|
}
|
|
118
|
-
if (
|
|
119
|
-
run.status === 'blocked' &&
|
|
120
|
-
failedProduction(fact, resolved, this.now())
|
|
121
|
-
) {
|
|
125
|
+
if (run.status === 'blocked' && failedProduction(fact, resolved, this.now())) {
|
|
122
126
|
run.readback = fact;
|
|
123
127
|
const cause = this.provider.productionFailure?.(fact);
|
|
124
128
|
if (cause) this.diagnostic(run.id, 'production_acceptance', cause);
|
|
@@ -130,7 +134,7 @@ export class ReleaseControlProducer extends ProducerExecution {
|
|
|
130
134
|
cause?.code || 'FUNCTIONAL_ACCEPTANCE_FAILED',
|
|
131
135
|
cause
|
|
132
136
|
? failureMessage(cause)
|
|
133
|
-
: 'Функциональная приёмка установленной версии не пройдена'
|
|
137
|
+
: 'Функциональная приёмка установленной версии не пройдена'
|
|
134
138
|
);
|
|
135
139
|
}
|
|
136
140
|
await this.save();
|
package/dist/provenance.json
CHANGED
|
@@ -1,27 +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
|
-
"src/exclusive-operation.mjs": "sha256:
|
|
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
|
-
"src/producer-admission.mjs": "sha256:
|
|
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:
|
|
20
|
-
"src/producer-publication.mjs": "sha256:
|
|
20
|
+
"src/producer-journal.mjs": "sha256:0934057702fb611b9a8d9dde58cb2c2024f436a49a4e5f6ab02f8884a320cea7",
|
|
21
|
+
"src/producer-publication-failure.mjs": "sha256:726b2b85880143b70219473896b894f64b25b1c495f77488f1e73ce950b839f6",
|
|
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:
|
|
26
|
+
"src/producer.mjs": "sha256:6d0b875a9901a14edf5b93c771356f5352e924ff7977c13b027343c9bdca42f4",
|
|
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:
|
|
36
|
+
"types/producer.d.ts": "sha256:980aad137365f6c131b6bfabfab411a73c4a2b2fb35a167152ef8ed446ec1ade",
|
|
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"
|