release-skill 0.6.2 → 0.7.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +48 -0
- package/CONTRIBUTING.md +1 -1
- package/INSTALL.md +47 -2
- package/INSTALL.zh-CN.md +29 -2
- package/README.md +137 -12
- package/README.zh-CN.md +110 -9
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/error-codes.json +1 -1
- package/adapters/claude/bin/kernel-protocol.json +1 -1
- package/adapters/claude/bin/license-texts/Apache-2.0.txt +201 -0
- package/adapters/claude/bin/license-texts/MIT.txt +21 -0
- package/adapters/claude/bin/registry.json +64 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +50570 -42208
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/schemas/.render-manifest.json +10 -6
- package/adapters/claude/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/claude/schemas/release-plan.schema.json +200 -4
- package/adapters/claude/schemas/release-project.schema.json +86 -5
- package/adapters/claude/schemas/release-run.schema.json +11 -6
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/error-codes.json +1 -1
- package/adapters/codex/bin/kernel-protocol.json +1 -1
- package/adapters/codex/bin/license-texts/Apache-2.0.txt +201 -0
- package/adapters/codex/bin/license-texts/MIT.txt +21 -0
- package/adapters/codex/bin/registry.json +64 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +50570 -42208
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/schemas/.render-manifest.json +10 -6
- package/adapters/codex/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/codex/schemas/release-plan.schema.json +200 -4
- package/adapters/codex/schemas/release-project.schema.json +86 -5
- package/adapters/codex/schemas/release-run.schema.json +11 -6
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/error-codes.json +1 -1
- package/adapters/kimi/bin/kernel-protocol.json +1 -1
- package/adapters/kimi/bin/license-texts/Apache-2.0.txt +201 -0
- package/adapters/kimi/bin/license-texts/MIT.txt +21 -0
- package/adapters/kimi/bin/registry.json +64 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +50570 -42208
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/schemas/.render-manifest.json +10 -6
- package/adapters/kimi/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/kimi/schemas/release-plan.schema.json +200 -4
- package/adapters/kimi/schemas/release-project.schema.json +86 -5
- package/adapters/kimi/schemas/release-run.schema.json +11 -6
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/error-codes.json +1 -1
- package/adapters/workbuddy/bin/kernel-protocol.json +1 -1
- package/adapters/workbuddy/bin/license-texts/Apache-2.0.txt +201 -0
- package/adapters/workbuddy/bin/license-texts/MIT.txt +21 -0
- package/adapters/workbuddy/bin/registry.json +64 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +50570 -42208
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/schemas/.render-manifest.json +10 -6
- package/adapters/workbuddy/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/workbuddy/schemas/release-plan.schema.json +200 -4
- package/adapters/workbuddy/schemas/release-project.schema.json +86 -5
- package/adapters/workbuddy/schemas/release-run.schema.json +11 -6
- package/bin/error-codes.json +1 -1
- package/bin/kernel-protocol.json +1 -1
- package/bin/license-texts/Apache-2.0.txt +201 -0
- package/bin/license-texts/MIT.txt +21 -0
- package/bin/registry.json +64 -1
- package/bin/release-skill-cli.mjs +187 -9
- package/bin/release-skill.bundle.mjs +50570 -42208
- package/bin/rules.json +1 -1
- package/package.json +6 -4
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +5 -5
- package/references/01-state-machine.md +22 -2
- package/references/02-project-config.md +1 -1
- package/references/06-adapter-contract.md +6 -3
- package/schemas/.render-manifest.json +10 -6
- package/schemas/postpublish-approval-record.schema.json +47 -0
- package/schemas/release-plan.schema.json +200 -4
- package/schemas/release-project.schema.json +86 -5
- package/schemas/release-run.schema.json +11 -6
- package/src/adapters/distribute-git.mjs +17 -11
- package/src/commands/approve.mjs +163 -1
- package/src/commands/distribute.mjs +649 -89
- package/src/commands/postverify.mjs +808 -0
- package/src/commands/prepare.mjs +456 -45
- package/src/commands/setup.mjs +715 -0
- package/src/commands/ship.mjs +152 -5
- package/src/commands/verify.mjs +92 -15
- package/src/core/approval.mjs +93 -68
- package/src/core/bounded-output.mjs +46 -0
- package/src/core/derived-artifact-gates.mjs +258 -0
- package/src/core/docs-refresh-preset.mjs +171 -0
- package/src/core/errors.mjs +4 -0
- package/src/core/evidence.mjs +8 -1
- package/src/core/git-url-policy.mjs +214 -0
- package/src/core/hooks.mjs +28 -0
- package/src/core/marketplace-registry-entry.mjs +175 -0
- package/src/core/notify-handoff.mjs +76 -0
- package/src/core/postpublish-approval.mjs +241 -0
- package/src/core/postpublish-bundle.mjs +342 -0
- package/src/core/postpublish-projection.mjs +210 -0
- package/src/core/postpublish.mjs +449 -25
- package/src/core/preset-executor.mjs +175 -0
- package/src/core/preset-gitwrite.mjs +484 -0
- package/src/core/presets.mjs +754 -0
- package/src/core/proposal-inbox.mjs +646 -0
- package/src/core/redact.mjs +16 -3
- package/src/core/run.mjs +91 -6
- package/src/producers/build-adapters.mjs +28 -14
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"PARTIAL",
|
|
44
44
|
"BLOCKED",
|
|
45
45
|
"DISTRIBUTING",
|
|
46
|
-
"DISTRIBUTED"
|
|
46
|
+
"DISTRIBUTED",
|
|
47
|
+
"NEEDS_INPUT"
|
|
47
48
|
],
|
|
48
49
|
"description": "Current status of the run"
|
|
49
50
|
},
|
|
@@ -55,7 +56,8 @@
|
|
|
55
56
|
"publish",
|
|
56
57
|
"reconcile",
|
|
57
58
|
"verify",
|
|
58
|
-
"distribute"
|
|
59
|
+
"distribute",
|
|
60
|
+
"postverify"
|
|
59
61
|
],
|
|
60
62
|
"description": "The command that initiated this run"
|
|
61
63
|
},
|
|
@@ -133,7 +135,8 @@
|
|
|
133
135
|
"codebuddy-marketplace-install",
|
|
134
136
|
"set-default-branch",
|
|
135
137
|
"distribute-probe",
|
|
136
|
-
"distribute-mirror"
|
|
138
|
+
"distribute-mirror",
|
|
139
|
+
"postpublish-hook"
|
|
137
140
|
],
|
|
138
141
|
"description": "Type of external action"
|
|
139
142
|
},
|
|
@@ -149,9 +152,10 @@
|
|
|
149
152
|
"PASSED_AUTOMATIC",
|
|
150
153
|
"PASSED_MANUAL",
|
|
151
154
|
"NOT_REQUIRED_UNCHANGED",
|
|
152
|
-
"NO_CHANGE"
|
|
155
|
+
"NO_CHANGE",
|
|
156
|
+
"AWAITING_APPROVAL"
|
|
153
157
|
],
|
|
154
|
-
"description": "Result status of this checkpoint (uncertain = started, result unknown; deferred = marketplace action postponed to verify phase; PASSED_AUTOMATIC/PASSED_MANUAL/NOT_REQUIRED_UNCHANGED 为验证阶段专用状态; NO_CHANGE = distribute 目标暂存差异为空,未产生 commit/tag/push)"
|
|
158
|
+
"description": "Result status of this checkpoint (uncertain = started, result unknown; deferred = marketplace action postponed to verify phase; PASSED_AUTOMATIC/PASSED_MANUAL/NOT_REQUIRED_UNCHANGED 为验证阶段专用状态; NO_CHANGE = distribute 目标暂存差异为空,未产生 commit/tag/push; AWAITING_APPROVAL = requiresApproval postPublish hook 尚无 checkpoint 批准,v0.6.3 R1)"
|
|
155
159
|
},
|
|
156
160
|
"preObserve": {
|
|
157
161
|
"type": "string",
|
|
@@ -621,7 +625,8 @@
|
|
|
621
625
|
"enum": [
|
|
622
626
|
"reconcile",
|
|
623
627
|
"verify",
|
|
624
|
-
"distribute"
|
|
628
|
+
"distribute",
|
|
629
|
+
"postverify"
|
|
625
630
|
]
|
|
626
631
|
}
|
|
627
632
|
},
|
|
@@ -50,6 +50,12 @@ import {
|
|
|
50
50
|
createResult,
|
|
51
51
|
} from './contract.mjs';
|
|
52
52
|
import { ReleaseError, GATE_FAILED, REMOTE_CONFLICT, REMOTE_UNAVAILABLE } from '../core/errors.mjs';
|
|
53
|
+
import {
|
|
54
|
+
checkGitRemoteUrl,
|
|
55
|
+
describeGitRemoteUrlFailure,
|
|
56
|
+
isAllowedGitRemoteUrl,
|
|
57
|
+
redactUrlCredentialsIfPresent,
|
|
58
|
+
} from '../core/git-url-policy.mjs';
|
|
53
59
|
|
|
54
60
|
const execFile = promisify(execFileCb);
|
|
55
61
|
|
|
@@ -59,8 +65,6 @@ const GIT_TIMEOUT_MS = 120_000;
|
|
|
59
65
|
const TRANSFER_TIMEOUT_MS = 300_000;
|
|
60
66
|
const TMP_PREFIX = 'release-skill-distribute-';
|
|
61
67
|
|
|
62
|
-
/** Remote URL pattern: http(s) or file (test transport), .git-suffixed. */
|
|
63
|
-
const SAFE_REMOTE_URL_RE = /^(?:https?|file):\/\/.+\.git$/;
|
|
64
68
|
/** Branch pattern (leading alphanumeric blocks option-like names). */
|
|
65
69
|
const SAFE_BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
66
70
|
/** Safe tag characters (belt + braces; prepare already rendered the tag). */
|
|
@@ -164,8 +168,8 @@ export function renderMarketplaceIndex(marketplace, params = {}) {
|
|
|
164
168
|
...(sha ? { sha } : {}),
|
|
165
169
|
};
|
|
166
170
|
} else {
|
|
167
|
-
if (typeof dependencyUrl !== 'string' || !
|
|
168
|
-
fail('url form requires the payload-mirror dependency remoteUrl', {
|
|
171
|
+
if (typeof dependencyUrl !== 'string' || !isAllowedGitRemoteUrl(dependencyUrl)) {
|
|
172
|
+
fail('url form requires the payload-mirror dependency remoteUrl', {});
|
|
169
173
|
}
|
|
170
174
|
source = {
|
|
171
175
|
source: 'url',
|
|
@@ -187,11 +191,12 @@ function hasControlChars(value) {
|
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
function assertSafeRemoteUrl(remoteUrl) {
|
|
190
|
-
|
|
194
|
+
const verdict = checkGitRemoteUrl(remoteUrl);
|
|
195
|
+
if (!verdict.ok) {
|
|
191
196
|
throw new ReleaseError(
|
|
192
197
|
GATE_FAILED,
|
|
193
|
-
|
|
194
|
-
{
|
|
198
|
+
`distribute remoteUrl ${describeGitRemoteUrlFailure(verdict.reason)}`,
|
|
199
|
+
{ reason: verdict.reason },
|
|
195
200
|
);
|
|
196
201
|
}
|
|
197
202
|
}
|
|
@@ -302,9 +307,10 @@ export function createDistributeGitAdapter(deps = {}) {
|
|
|
302
307
|
function probeFailureResult(action, error) {
|
|
303
308
|
const classification = classifyProbeFailure(stderrText(error));
|
|
304
309
|
const code = classification === 'auth' ? REMOTE_CONFLICT : REMOTE_UNAVAILABLE;
|
|
310
|
+
const safeRemoteUrl = redactUrlCredentialsIfPresent(action.remoteUrl);
|
|
305
311
|
const message = classification === 'auth'
|
|
306
|
-
? `remote refused authentication for ${
|
|
307
|
-
: `cannot reach ${
|
|
312
|
+
? `remote refused authentication for ${safeRemoteUrl}; distribute reuses the host keychain credential and never prompts, reads, or retries credentials`
|
|
313
|
+
: `cannot reach ${safeRemoteUrl} — start VPN / check network; distribute fails closed and never retries with credentials`;
|
|
308
314
|
return createResult({
|
|
309
315
|
actionType: action.actionType,
|
|
310
316
|
status: ActionStatus.PREFLIGHT_FAILED,
|
|
@@ -312,7 +318,7 @@ export function createDistributeGitAdapter(deps = {}) {
|
|
|
312
318
|
details: {
|
|
313
319
|
code,
|
|
314
320
|
classification,
|
|
315
|
-
remoteUrl:
|
|
321
|
+
remoteUrl: safeRemoteUrl,
|
|
316
322
|
stderrTail: stderrTail(error),
|
|
317
323
|
},
|
|
318
324
|
});
|
|
@@ -468,7 +474,7 @@ export function createDistributeGitAdapter(deps = {}) {
|
|
|
468
474
|
if (existingTag) {
|
|
469
475
|
throw new ReleaseError(
|
|
470
476
|
REMOTE_CONFLICT,
|
|
471
|
-
`target ${action.remoteUrl} already carries tag ${action.tag} and the content differs; moving an existing tag would be force-equivalent — human decision required`,
|
|
477
|
+
`target ${redactUrlCredentialsIfPresent(action.remoteUrl)} already carries tag ${action.tag} and the content differs; moving an existing tag would be force-equivalent — human decision required`,
|
|
472
478
|
{ targetId: action.targetId, tag: action.tag },
|
|
473
479
|
);
|
|
474
480
|
}
|
package/src/commands/approve.mjs
CHANGED
|
@@ -25,7 +25,13 @@ import {
|
|
|
25
25
|
validatePlanActionCompleteness,
|
|
26
26
|
} from '../core/plan.mjs';
|
|
27
27
|
import { ReleaseError, PLAN_DIGEST_MISMATCH, GATE_FAILED } from '../core/errors.mjs';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
computeApprovalDigest,
|
|
30
|
+
validateApprovalRecordSchema,
|
|
31
|
+
MAX_APPROVAL_MS,
|
|
32
|
+
} from '../core/approval.mjs';
|
|
33
|
+
import { validatePostPublishApproval, derivePostPublishApprovalAuthorityPath } from '../core/postpublish-approval.mjs';
|
|
34
|
+
import { normalizePostPublishHook, effectiveHookRequiresApproval } from '../core/postpublish.mjs';
|
|
29
35
|
import { WORKSPACE_DIGEST_ALGORITHM } from '../core/baseline.mjs';
|
|
30
36
|
|
|
31
37
|
// ---------------------------------------------------------------------------
|
|
@@ -280,3 +286,159 @@ export async function approvePlan(options) {
|
|
|
280
286
|
latestApprovalPath: writePath,
|
|
281
287
|
});
|
|
282
288
|
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Approve a requiresApproval postPublish hook (checkpoint-level approval,
|
|
292
|
+
* v0.6.3 R1, design §2.7 ruling 2).
|
|
293
|
+
*
|
|
294
|
+
* The record binds (planDigest, hookId) — the plan-level approval schema is
|
|
295
|
+
* untouched. Hook config changes change the plan digest, so approvals
|
|
296
|
+
* invalidate naturally with the plan. The result carries the approved hook's
|
|
297
|
+
* normalized entry summary (review N-6) so the checkpoint approval interface
|
|
298
|
+
* can display exactly what was approved.
|
|
299
|
+
*
|
|
300
|
+
* @param {Object} options
|
|
301
|
+
* @param {string} options.planPath - Absolute path to the frozen release plan.
|
|
302
|
+
* @param {string} options.hookId - id of the declared hook to approve.
|
|
303
|
+
* @param {string} options.actor - Identity of the approver.
|
|
304
|
+
* @param {number} [options.expiresInMs] - Validity window in ms (default and max 24h).
|
|
305
|
+
* @param {() => string} [options.clock] - Clock function returning ISO-8601 strings.
|
|
306
|
+
* @param {string} [options.runId] - Audit-only distribute run id (never a binding).
|
|
307
|
+
*
|
|
308
|
+
* @returns {Promise<object>} { planDigest, hookId, actor, approvedAt,
|
|
309
|
+
* expiresAt, approvalDigest, approvalPath, hookSummary }.
|
|
310
|
+
*
|
|
311
|
+
* @throws {ReleaseError} GATE_FAILED when the hook is undeclared, does not
|
|
312
|
+
* require approval, or the window exceeds 24h.
|
|
313
|
+
*/
|
|
314
|
+
export async function approvePostPublishHook(options) {
|
|
315
|
+
const {
|
|
316
|
+
planPath,
|
|
317
|
+
hookId,
|
|
318
|
+
actor,
|
|
319
|
+
expiresInMs = MAX_APPROVAL_MS,
|
|
320
|
+
clock,
|
|
321
|
+
runId,
|
|
322
|
+
} = options ?? {};
|
|
323
|
+
|
|
324
|
+
const clockFn = typeof clock === 'function' ? clock : defaultClock;
|
|
325
|
+
|
|
326
|
+
if (!planPath || typeof planPath !== 'string') {
|
|
327
|
+
throw new ReleaseError(GATE_FAILED, 'planPath must be a non-empty string');
|
|
328
|
+
}
|
|
329
|
+
if (!hookId || typeof hookId !== 'string') {
|
|
330
|
+
throw new ReleaseError(GATE_FAILED, 'hookId must be a non-empty string');
|
|
331
|
+
}
|
|
332
|
+
if (!actor || typeof actor !== 'string') {
|
|
333
|
+
throw new ReleaseError(GATE_FAILED, 'actor must be a non-empty string');
|
|
334
|
+
}
|
|
335
|
+
if (typeof expiresInMs !== 'number' || !Number.isFinite(expiresInMs) || expiresInMs <= 0) {
|
|
336
|
+
throw new ReleaseError(GATE_FAILED, 'expiresInMs must be a positive finite number', { expiresInMs });
|
|
337
|
+
}
|
|
338
|
+
if (expiresInMs > MAX_APPROVAL_MS) {
|
|
339
|
+
throw new ReleaseError(
|
|
340
|
+
GATE_FAILED,
|
|
341
|
+
`expiresInMs must not exceed 24 hours (${MAX_APPROVAL_MS}ms), got ${expiresInMs}ms`,
|
|
342
|
+
{ expiresInMs },
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// --- Load and validate the frozen plan ---
|
|
347
|
+
let planRaw;
|
|
348
|
+
try {
|
|
349
|
+
planRaw = await readFile(planPath, 'utf8');
|
|
350
|
+
} catch (err) {
|
|
351
|
+
throw new ReleaseError(GATE_FAILED, `cannot read release plan: ${err.message}`, { planPath, cause: err.code });
|
|
352
|
+
}
|
|
353
|
+
let plan;
|
|
354
|
+
try {
|
|
355
|
+
plan = JSON.parse(planRaw);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
throw new ReleaseError(GATE_FAILED, `release plan is not valid JSON: ${err.message}`, { planPath });
|
|
358
|
+
}
|
|
359
|
+
const actualDigest = computePlanDigest(plan);
|
|
360
|
+
assertImmutablePlanAuthority(planPath, plan);
|
|
361
|
+
validatePlan(plan);
|
|
362
|
+
|
|
363
|
+
// --- Locate the declared hook (fail-closed) ---
|
|
364
|
+
const hooks = plan.postPublish?.hooks ?? [];
|
|
365
|
+
const hook = hooks.find((entry) => entry.id === hookId);
|
|
366
|
+
if (!hook) {
|
|
367
|
+
throw new ReleaseError(
|
|
368
|
+
GATE_FAILED,
|
|
369
|
+
`hook "${hookId}" is not declared in the frozen plan's postPublish hooks`,
|
|
370
|
+
{ hookId, declaredHookIds: hooks.map((entry) => entry.id) },
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
// Effective requiresApproval (§2.6 grading, R4 review M-1): preset hooks
|
|
374
|
+
// may inherit the preset-declared default (proposal-inbox git-push,
|
|
375
|
+
// declared OR inferred from a remoteUrl address -> true) without an
|
|
376
|
+
// explicit declaration. Grading by the literal value here would refuse to
|
|
377
|
+
// mint approvals for exactly the hooks the runtime gates — the mint and
|
|
378
|
+
// validatePostPublishApproval must agree on the effective value.
|
|
379
|
+
const effectiveRequiresApproval = effectiveHookRequiresApproval(hook);
|
|
380
|
+
if (effectiveRequiresApproval !== true) {
|
|
381
|
+
throw new ReleaseError(
|
|
382
|
+
GATE_FAILED,
|
|
383
|
+
`hook "${hookId}" does not require approval (effective requiresApproval is not true)`,
|
|
384
|
+
{ hookId, requiresApproval: effectiveRequiresApproval },
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// --- Build the checkpoint approval record ---
|
|
389
|
+
const approvedAt = clockFn();
|
|
390
|
+
const approvedAtDate = new Date(approvedAt);
|
|
391
|
+
if (Number.isNaN(approvedAtDate.getTime())) {
|
|
392
|
+
throw new ReleaseError(GATE_FAILED, `invalid approvedAt timestamp: "${approvedAt}"`, { approvedAt });
|
|
393
|
+
}
|
|
394
|
+
const expiresAt = new Date(approvedAtDate.getTime() + expiresInMs).toISOString();
|
|
395
|
+
|
|
396
|
+
const approvalRecord = {
|
|
397
|
+
planDigest: actualDigest,
|
|
398
|
+
hookId,
|
|
399
|
+
actor,
|
|
400
|
+
approvedAt,
|
|
401
|
+
expiresAt,
|
|
402
|
+
...(runId ? { runId } : {}),
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// Full fail-closed re-validation (schema + bindings + time window).
|
|
406
|
+
validatePostPublishApproval(plan, approvalRecord, { clock: clockFn });
|
|
407
|
+
|
|
408
|
+
// --- Write the digest-addressed authority ---
|
|
409
|
+
const json = JSON.stringify(approvalRecord, null, 2);
|
|
410
|
+
const approvalDigest = computeApprovalDigest(json);
|
|
411
|
+
// Single authority-layout source (core/postpublish-approval.mjs): the
|
|
412
|
+
// consumer-side assertion derives the exact same path fail-closed.
|
|
413
|
+
const { authorityPath: immutableApprovalPath } = derivePostPublishApprovalAuthorityPath(
|
|
414
|
+
planPath,
|
|
415
|
+
actualDigest,
|
|
416
|
+
approvalDigest,
|
|
417
|
+
);
|
|
418
|
+
await prepareAuthorityDirectory(dirname(immutableApprovalPath));
|
|
419
|
+
await assertAuthorityFileTarget(immutableApprovalPath);
|
|
420
|
+
try {
|
|
421
|
+
await writeFile(immutableApprovalPath, json, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (error.code !== 'EEXIST') throw error;
|
|
424
|
+
const existing = await readFile(immutableApprovalPath, 'utf8');
|
|
425
|
+
if (existing !== json) {
|
|
426
|
+
throw new ReleaseError(
|
|
427
|
+
GATE_FAILED,
|
|
428
|
+
'postpublish approval authority digest collision: existing bytes differ',
|
|
429
|
+
{ planDigest: actualDigest, approvalDigest, immutableApprovalPath },
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Review N-6: the normalized entry summary is the display surface of the
|
|
435
|
+
// checkpoint approval interface (defaults applied, digest-bound).
|
|
436
|
+
const hookSummary = normalizePostPublishHook(hook);
|
|
437
|
+
|
|
438
|
+
return Object.freeze({
|
|
439
|
+
...approvalRecord,
|
|
440
|
+
approvalDigest,
|
|
441
|
+
approvalPath: immutableApprovalPath,
|
|
442
|
+
hookSummary,
|
|
443
|
+
});
|
|
444
|
+
}
|