release-skill 0.2.5 → 0.2.7
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 +42 -0
- package/INSTALL.md +40 -16
- package/INSTALL.zh-CN.md +34 -12
- package/README.md +42 -15
- package/README.zh-CN.md +36 -15
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +2671 -1552
- package/adapters/claude/schemas/.render-manifest.json +6 -6
- package/adapters/claude/schemas/release-plan.schema.json +67 -0
- package/adapters/claude/schemas/release-project.schema.json +6 -0
- package/adapters/claude/schemas/release-run.schema.json +65 -0
- package/adapters/claude/skills/release-prepare/SKILL.md +5 -0
- package/adapters/claude/skills/release-setup/SKILL.md +10 -1
- package/adapters/claude/skills/release-verify/SKILL.md +4 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +2671 -1552
- package/adapters/codex/schemas/.render-manifest.json +6 -6
- package/adapters/codex/schemas/release-plan.schema.json +67 -0
- package/adapters/codex/schemas/release-project.schema.json +6 -0
- package/adapters/codex/schemas/release-run.schema.json +65 -0
- package/adapters/codex/skills/release-prepare/SKILL.md +5 -0
- package/adapters/codex/skills/release-setup/SKILL.md +10 -1
- package/adapters/codex/skills/release-verify/SKILL.md +4 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +2671 -1552
- package/adapters/kimi/schemas/.render-manifest.json +6 -6
- package/adapters/kimi/schemas/release-plan.schema.json +67 -0
- package/adapters/kimi/schemas/release-project.schema.json +6 -0
- package/adapters/kimi/schemas/release-run.schema.json +65 -0
- package/adapters/kimi/skills/release-prepare/SKILL.md +5 -0
- package/adapters/kimi/skills/release-setup/SKILL.md +10 -1
- package/adapters/kimi/skills/release-verify/SKILL.md +4 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +2671 -1552
- package/adapters/workbuddy/schemas/.render-manifest.json +6 -6
- package/adapters/workbuddy/schemas/release-plan.schema.json +67 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +6 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +65 -0
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +5 -0
- package/adapters/workbuddy/skills/release-setup/SKILL.md +10 -1
- package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -2
- package/bin/release-skill.bundle.mjs +2671 -1552
- package/package.json +1 -1
- package/references/.render-manifest.json +4 -4
- package/references/02-project-config.md +28 -2
- package/references/05-evidence-and-errors.md +6 -0
- package/schemas/.render-manifest.json +6 -6
- package/schemas/release-plan.schema.json +67 -0
- package/schemas/release-project.schema.json +6 -0
- package/schemas/release-run.schema.json +65 -0
- package/skills/release-prepare/SKILL.md +5 -0
- package/skills/release-setup/SKILL.md +10 -1
- package/skills/release-verify/SKILL.md +4 -2
- package/skills-src/release-prepare/SKILL.md +5 -0
- package/skills-src/release-setup/SKILL.md +10 -1
- package/skills-src/release-verify/SKILL.md +4 -2
- package/src/adapters/npm.mjs +54 -2
- package/src/adapters/plugin-marketplace.mjs +4 -29
- package/src/commands/prepare.mjs +161 -4
- package/src/commands/publish.mjs +74 -2
- package/src/commands/reconcile.mjs +58 -0
- package/src/commands/setup.mjs +194 -0
- package/src/commands/verify.mjs +60 -1
- package/src/core/errors.mjs +12 -0
- package/src/core/source-authority.mjs +547 -0
- package/src/npm/npm-entry-closure.mjs +195 -0
- package/src/platforms/codebuddy.mjs +19 -11
- package/src/platforms/codex.mjs +18 -10
- package/src/platforms/kimi.mjs +26 -39
- package/src/snapshot/frozen.mjs +51 -19
package/src/commands/prepare.mjs
CHANGED
|
@@ -49,10 +49,16 @@ import {
|
|
|
49
49
|
normalizeGitTimestamp,
|
|
50
50
|
sealFrozenSnapshot,
|
|
51
51
|
} from '../snapshot/frozen.mjs';
|
|
52
|
-
import { ReleaseError, GATE_FAILED, CONFIG_INVALID, FORBIDDEN_CONTENT_DETECTED, RELEASE_DOCS_STALE } from '../core/errors.mjs';
|
|
52
|
+
import { ReleaseError, GATE_FAILED, CONFIG_INVALID, CONFIG_MISSING, FORBIDDEN_CONTENT_DETECTED, RELEASE_DOCS_STALE, DIRTY_SOURCE_INPUT } from '../core/errors.mjs';
|
|
53
|
+
import {
|
|
54
|
+
SOURCE_INPUT_ALGORITHM_VERSION,
|
|
55
|
+
computeSourceInputClosure,
|
|
56
|
+
checkSourceInputDirty,
|
|
57
|
+
verifySnapshotSourcesMatchClosure,
|
|
58
|
+
} from '../core/source-authority.mjs';
|
|
53
59
|
import { acquireProjectLock } from '../artifacts/project-lock.mjs';
|
|
54
60
|
import { assertPreviousPublicBaselineTarget, observePreviousPublicBaseline } from '../core/previous-public-baseline.mjs';
|
|
55
|
-
import {
|
|
61
|
+
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
56
62
|
import { createProductionPrepareRunDir } from '../core/run.mjs';
|
|
57
63
|
import { PLATFORMS } from '../platforms/registry.mjs';
|
|
58
64
|
import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
|
|
@@ -924,13 +930,13 @@ async function buildProductionAssets(
|
|
|
924
930
|
tarballDir: resolveUnitScopedPath(resolve(runDir, 'tarballs'), unit.id),
|
|
925
931
|
expectedSnapshotDigest: sealed.digest,
|
|
926
932
|
});
|
|
927
|
-
await
|
|
933
|
+
await verifyFrozenNpmTarballContract({
|
|
928
934
|
package: npmDistribution.package,
|
|
929
935
|
version,
|
|
930
936
|
tarballPath: relative(root, npm.tarballPath),
|
|
931
937
|
tarballSha256: npm.sha256,
|
|
932
938
|
integrity: npm.integrity,
|
|
933
|
-
}, root);
|
|
939
|
+
}, root, resolveUnitScopedPath(resolve(runDir, 'tarballs'), unit.id));
|
|
934
940
|
}
|
|
935
941
|
|
|
936
942
|
assets.push({
|
|
@@ -1919,6 +1925,87 @@ export async function prepareRelease(options) {
|
|
|
1919
1925
|
});
|
|
1920
1926
|
}
|
|
1921
1927
|
|
|
1928
|
+
// --- Step 3c: Source authority content closure gate ---
|
|
1929
|
+
// After hooks complete, compute the deterministic source-input closure
|
|
1930
|
+
// and verify that closure inputs are clean (no staged/unstaged/untracked
|
|
1931
|
+
// changes). Production configs must declare sourceRepository.
|
|
1932
|
+
const sourceRepository = config.project?.sourceRepository ?? null;
|
|
1933
|
+
const configDefaultBranch = config.project?.defaultBranch ?? null;
|
|
1934
|
+
|
|
1935
|
+
let sourceAuthority = null;
|
|
1936
|
+
let sourceInputClosure = null;
|
|
1937
|
+
if (production) {
|
|
1938
|
+
if (!sourceRepository || typeof sourceRepository !== 'string') {
|
|
1939
|
+
throw new ReleaseError(
|
|
1940
|
+
CONFIG_MISSING,
|
|
1941
|
+
'production prepare requires project.sourceRepository in configuration; source authority content gate needs a workspace source repository',
|
|
1942
|
+
{ configPath },
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
await evidence.append({ phase: 'source-authority', status: 'started' });
|
|
1947
|
+
|
|
1948
|
+
// Compute source-input closure from resolved unit configs
|
|
1949
|
+
const unitConfigsForClosure = configUnits.map((unit, idx) => ({
|
|
1950
|
+
...unit,
|
|
1951
|
+
version: { ...unit.version },
|
|
1952
|
+
}));
|
|
1953
|
+
sourceInputClosure = await computeSourceInputClosure({
|
|
1954
|
+
units: unitConfigsForClosure,
|
|
1955
|
+
root: realRoot,
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
await evidence.append({
|
|
1959
|
+
phase: 'source-authority',
|
|
1960
|
+
step: 'closure-computed',
|
|
1961
|
+
entryCount: sourceInputClosure.entries.length,
|
|
1962
|
+
inputDigest: sourceInputClosure.digest,
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
// Check only closure inputs for dirty (not whole workspace)
|
|
1966
|
+
const dirtyResult = await checkSourceInputDirty({
|
|
1967
|
+
closure: sourceInputClosure,
|
|
1968
|
+
root: realRoot,
|
|
1969
|
+
});
|
|
1970
|
+
if (dirtyResult.dirty) {
|
|
1971
|
+
await evidence.append({
|
|
1972
|
+
phase: 'source-authority',
|
|
1973
|
+
status: 'blocking',
|
|
1974
|
+
reason: 'DIRTY_SOURCE_INPUT',
|
|
1975
|
+
dirtyPaths: dirtyResult.dirtyPaths,
|
|
1976
|
+
});
|
|
1977
|
+
throw new ReleaseError(
|
|
1978
|
+
DIRTY_SOURCE_INPUT,
|
|
1979
|
+
`source-input closure files have uncommitted changes: ${dirtyResult.dirtyPaths.join(', ')}`,
|
|
1980
|
+
{ dirtyPaths: dirtyResult.dirtyPaths },
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
await evidence.append({
|
|
1985
|
+
phase: 'source-authority',
|
|
1986
|
+
step: 'dirty-check',
|
|
1987
|
+
status: 'clean',
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
// Build sourceAuthority binding for plan digest
|
|
1991
|
+
sourceAuthority = {
|
|
1992
|
+
sourceRepository,
|
|
1993
|
+
defaultBranch: configDefaultBranch,
|
|
1994
|
+
entries: sourceInputClosure.entries,
|
|
1995
|
+
inputDigest: sourceInputClosure.digest,
|
|
1996
|
+
algorithmVersion: SOURCE_INPUT_ALGORITHM_VERSION,
|
|
1997
|
+
};
|
|
1998
|
+
|
|
1999
|
+
await evidence.append({
|
|
2000
|
+
phase: 'source-authority',
|
|
2001
|
+
status: 'completed',
|
|
2002
|
+
sourceRepository,
|
|
2003
|
+
defaultBranch: configDefaultBranch,
|
|
2004
|
+
inputDigest: sourceInputClosure.digest,
|
|
2005
|
+
remoteObservation: offline ? 'unobserved-offline' : 'deferred-to-publish',
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1922
2009
|
// --- Step 4: Capture Git baseline (AFTER hooks, so workspaceDigest
|
|
1923
2010
|
// reflects any file changes introduced by hooks) ---
|
|
1924
2011
|
await evidence.append({ phase: 'baseline', status: 'started' });
|
|
@@ -2183,6 +2270,75 @@ export async function prepareRelease(options) {
|
|
|
2183
2270
|
gateCount: snapshotGateResults.length,
|
|
2184
2271
|
});
|
|
2185
2272
|
|
|
2273
|
+
// Bind the remote source-authority proof to the exact bytes that entered
|
|
2274
|
+
// the frozen snapshots, not merely to an earlier read of the workspace.
|
|
2275
|
+
// Then re-read the complete closure and dirty state once more so version
|
|
2276
|
+
// sources and non-snapshot closure entries cannot drift during prepare.
|
|
2277
|
+
if (production) {
|
|
2278
|
+
const snapshotSourceResult = verifySnapshotSourcesMatchClosure({
|
|
2279
|
+
closure: sourceInputClosure,
|
|
2280
|
+
unitResults,
|
|
2281
|
+
});
|
|
2282
|
+
if (!snapshotSourceResult.passed) {
|
|
2283
|
+
throw new ReleaseError(
|
|
2284
|
+
DIRTY_SOURCE_INPUT,
|
|
2285
|
+
'source inputs changed between closure calculation and frozen snapshot construction',
|
|
2286
|
+
{
|
|
2287
|
+
reason: 'SNAPSHOT_SOURCE_DRIFT',
|
|
2288
|
+
dirtyPaths: snapshotSourceResult.error.paths,
|
|
2289
|
+
},
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
const finalClosure = await computeSourceInputClosure({
|
|
2294
|
+
units: configUnits,
|
|
2295
|
+
root: realRoot,
|
|
2296
|
+
});
|
|
2297
|
+
if (finalClosure.digest !== sourceInputClosure.digest) {
|
|
2298
|
+
const initialByPath = new Map(
|
|
2299
|
+
sourceInputClosure.entries.map((entry) => [entry.path, entry]),
|
|
2300
|
+
);
|
|
2301
|
+
const finalByPath = new Map(
|
|
2302
|
+
finalClosure.entries.map((entry) => [entry.path, entry]),
|
|
2303
|
+
);
|
|
2304
|
+
const changedPaths = [...new Set([
|
|
2305
|
+
...sourceInputClosure.entries.map((entry) => entry.path),
|
|
2306
|
+
...finalClosure.entries.map((entry) => entry.path),
|
|
2307
|
+
])].filter((path) => (
|
|
2308
|
+
JSON.stringify(initialByPath.get(path) ?? null)
|
|
2309
|
+
!== JSON.stringify(finalByPath.get(path) ?? null)
|
|
2310
|
+
)).sort();
|
|
2311
|
+
throw new ReleaseError(
|
|
2312
|
+
DIRTY_SOURCE_INPUT,
|
|
2313
|
+
'source-input closure changed while preparing frozen snapshots',
|
|
2314
|
+
{ reason: 'SOURCE_CLOSURE_DRIFT', dirtyPaths: changedPaths },
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
const finalDirtyResult = await checkSourceInputDirty({
|
|
2319
|
+
closure: finalClosure,
|
|
2320
|
+
root: realRoot,
|
|
2321
|
+
});
|
|
2322
|
+
if (finalDirtyResult.dirty) {
|
|
2323
|
+
throw new ReleaseError(
|
|
2324
|
+
DIRTY_SOURCE_INPUT,
|
|
2325
|
+
`source-input closure files have uncommitted changes after snapshot construction: ${finalDirtyResult.dirtyPaths.join(', ')}`,
|
|
2326
|
+
{
|
|
2327
|
+
reason: 'DIRTY_AFTER_SNAPSHOT',
|
|
2328
|
+
dirtyPaths: finalDirtyResult.dirtyPaths,
|
|
2329
|
+
},
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
await evidence.append({
|
|
2334
|
+
phase: 'source-authority',
|
|
2335
|
+
step: 'snapshot-binding',
|
|
2336
|
+
status: 'completed',
|
|
2337
|
+
inputDigest: sourceInputClosure.digest,
|
|
2338
|
+
snapshotSourceCount: snapshotSourceResult.observation.snapshotSourceCount,
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2186
2342
|
// --- Step 6: Remote uniqueness (deferred to publish preflight) ---
|
|
2187
2343
|
// Prepare only observes the previous public baseline (already done above).
|
|
2188
2344
|
// Remote uniqueness checks (tag, GitHub Release, npm version) are deferred
|
|
@@ -2665,6 +2821,7 @@ export async function prepareRelease(options) {
|
|
|
2665
2821
|
} : {}),
|
|
2666
2822
|
units,
|
|
2667
2823
|
externalActions,
|
|
2824
|
+
...(sourceAuthority ? { sourceAuthority } : {}),
|
|
2668
2825
|
createdAt: production ? createdAtTimestamp : (clock ? clock() : new Date().toISOString()),
|
|
2669
2826
|
};
|
|
2670
2827
|
|
package/src/commands/publish.mjs
CHANGED
|
@@ -59,10 +59,15 @@ import { appendRunState, createProductionRunDir, writeRunAtomic, resolveDefaultR
|
|
|
59
59
|
import {
|
|
60
60
|
ReleaseError,
|
|
61
61
|
GATE_FAILED,
|
|
62
|
+
CONFIG_MISSING,
|
|
62
63
|
BASELINE_CHANGED,
|
|
63
64
|
PARTIAL_RELEASE,
|
|
64
65
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
65
66
|
} from '../core/errors.mjs';
|
|
67
|
+
import {
|
|
68
|
+
verifyRemoteSourceContent,
|
|
69
|
+
createSourceAuthorityReceipt,
|
|
70
|
+
} from '../core/source-authority.mjs';
|
|
66
71
|
import { assertTransition, PUBLISHING, PUBLISHED, PARTIAL } from '../core/state-machine.mjs';
|
|
67
72
|
import { matchObservation } from '../adapters/contract.mjs';
|
|
68
73
|
import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY, isPropagatingMissing } from '../core/observe-retry.mjs';
|
|
@@ -72,7 +77,7 @@ import {
|
|
|
72
77
|
verifyFrozenGitRepository,
|
|
73
78
|
verifyFrozenSnapshot,
|
|
74
79
|
} from '../snapshot/frozen.mjs';
|
|
75
|
-
import {
|
|
80
|
+
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
76
81
|
|
|
77
82
|
function assertInsideAssetRoot(assetRoot, candidate, label) {
|
|
78
83
|
const rel = relative(assetRoot, candidate);
|
|
@@ -527,6 +532,13 @@ export async function publishRelease(options) {
|
|
|
527
532
|
if (productionMode && !isProductionPlan) {
|
|
528
533
|
throw new ReleaseError(GATE_FAILED, 'production publish requires a github-npm-v1 frozen plan');
|
|
529
534
|
}
|
|
535
|
+
if (isProductionPlan && !plan.sourceAuthority) {
|
|
536
|
+
throw new ReleaseError(
|
|
537
|
+
CONFIG_MISSING,
|
|
538
|
+
'production publish requires a frozen sourceAuthority binding; re-run prepare with project.sourceRepository configured',
|
|
539
|
+
{ gate: 'source-authority' },
|
|
540
|
+
);
|
|
541
|
+
}
|
|
530
542
|
const verifiedFrozenSnapshots = new Map();
|
|
531
543
|
if (isProductionPlan) {
|
|
532
544
|
if (!plan.production.assetRoot || plan.production.assetRoot === '.') {
|
|
@@ -595,7 +607,7 @@ export async function publishRelease(options) {
|
|
|
595
607
|
if (!npmDistribution) {
|
|
596
608
|
throw new ReleaseError(GATE_FAILED, `unit "${unit.id}" has a frozen npm tarball but no npm distribution`);
|
|
597
609
|
}
|
|
598
|
-
await
|
|
610
|
+
await verifyFrozenNpmTarballContract({
|
|
599
611
|
package: npmDistribution.package,
|
|
600
612
|
version: unit.targetVersion,
|
|
601
613
|
tarballPath: frozen.npm.tarballPath,
|
|
@@ -932,6 +944,65 @@ export async function publishRelease(options) {
|
|
|
932
944
|
}
|
|
933
945
|
}
|
|
934
946
|
|
|
947
|
+
// =======================================================================
|
|
948
|
+
// Safety Gate 11: Source authority content closure verification
|
|
949
|
+
// Verifies that the frozen source-input closure content exists in the
|
|
950
|
+
// workspace's remote default branch. Must pass before ANY adapter execute.
|
|
951
|
+
// Failure => zero adapter execute calls.
|
|
952
|
+
// =======================================================================
|
|
953
|
+
let sourceAuthorityReceipt = null;
|
|
954
|
+
if (plan.sourceAuthority) {
|
|
955
|
+
await evidence.append({ phase: 'safety-gate', gate: 'source-authority', status: 'started' });
|
|
956
|
+
|
|
957
|
+
const sa = plan.sourceAuthority;
|
|
958
|
+
|
|
959
|
+
// Verify the remote source tree against the entries frozen into the
|
|
960
|
+
// digest-bound plan. Publish must never rebuild authority from the
|
|
961
|
+
// mutable workspace or current config.
|
|
962
|
+
const frozenClosure = {
|
|
963
|
+
algorithmVersion: sa.algorithmVersion,
|
|
964
|
+
digest: sa.inputDigest,
|
|
965
|
+
entries: sa.entries,
|
|
966
|
+
};
|
|
967
|
+
const remoteResult = await verifyRemoteSourceContent({
|
|
968
|
+
sourceRepository: sa.sourceRepository,
|
|
969
|
+
defaultBranch: sa.defaultBranch,
|
|
970
|
+
closure: frozenClosure,
|
|
971
|
+
readRemoteFn: options.readRemoteSourceFn,
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
if (!remoteResult.passed) {
|
|
975
|
+
const errorCode = remoteResult.error?.code ?? GATE_FAILED;
|
|
976
|
+
await evidence.append({
|
|
977
|
+
phase: 'safety-gate',
|
|
978
|
+
gate: 'source-authority',
|
|
979
|
+
status: 'failed',
|
|
980
|
+
error: remoteResult.error,
|
|
981
|
+
});
|
|
982
|
+
throw new ReleaseError(
|
|
983
|
+
errorCode,
|
|
984
|
+
`source authority content gate failed: ${remoteResult.error?.message}`,
|
|
985
|
+
remoteResult.error,
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Create receipt for successful verification
|
|
990
|
+
sourceAuthorityReceipt = createSourceAuthorityReceipt({
|
|
991
|
+
plan,
|
|
992
|
+
result: 'CONSISTENT',
|
|
993
|
+
observation: remoteResult.observation,
|
|
994
|
+
clock: clockFn,
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
await evidence.append({
|
|
998
|
+
phase: 'safety-gate',
|
|
999
|
+
gate: 'source-authority',
|
|
1000
|
+
status: 'passed',
|
|
1001
|
+
sourceRepository: sa.sourceRepository,
|
|
1002
|
+
defaultBranch: sa.defaultBranch,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
935
1006
|
// =======================================================================
|
|
936
1007
|
// All safety gates passed -- prepare for execution
|
|
937
1008
|
// =======================================================================
|
|
@@ -1069,6 +1140,7 @@ export async function publishRelease(options) {
|
|
|
1069
1140
|
: {}),
|
|
1070
1141
|
})),
|
|
1071
1142
|
startedAt,
|
|
1143
|
+
...(sourceAuthorityReceipt ? { sourceAuthorityReceipts: [sourceAuthorityReceipt] } : {}),
|
|
1072
1144
|
...(finishedAt ? { finishedAt } : {}),
|
|
1073
1145
|
});
|
|
1074
1146
|
let stateSequence = 0;
|
|
@@ -59,8 +59,10 @@ import {
|
|
|
59
59
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
60
60
|
} from '../core/errors.mjs';
|
|
61
61
|
import { assertTransition, PARTIAL, PUBLISHED, BLOCKED } from '../core/state-machine.mjs';
|
|
62
|
+
import { verifySourceAuthorityReceipt } from '../core/source-authority.mjs';
|
|
62
63
|
import { matchObservation } from '../adapters/contract.mjs';
|
|
63
64
|
import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY } from '../core/observe-retry.mjs';
|
|
65
|
+
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
64
66
|
|
|
65
67
|
// ---------------------------------------------------------------------------
|
|
66
68
|
// Constants
|
|
@@ -209,6 +211,44 @@ export async function reconcileRelease(options) {
|
|
|
209
211
|
|
|
210
212
|
await evidence.append({ phase: 'safety-gate', gate: 'action-completeness', status: 'passed' });
|
|
211
213
|
|
|
214
|
+
// =======================================================================
|
|
215
|
+
// Safety Gate 2c: Unconditional frozen npm entry-closure recheck
|
|
216
|
+
// This is plan-global and intentionally precedes source-run observation:
|
|
217
|
+
// a previously consistent npm checkpoint must not exempt a bad frozen
|
|
218
|
+
// tarball while another action is being reconciled.
|
|
219
|
+
// =======================================================================
|
|
220
|
+
for (const unit of plan.units ?? []) {
|
|
221
|
+
const frozen = unit.frozenSnapshot;
|
|
222
|
+
if (!frozen?.npm) continue;
|
|
223
|
+
const npmDistribution = (unit.distributions ?? []).find((distribution) => distribution.type === 'npm');
|
|
224
|
+
if (!npmDistribution) {
|
|
225
|
+
throw new ReleaseError(
|
|
226
|
+
GATE_FAILED,
|
|
227
|
+
`unit "${unit.id}" has a frozen npm tarball but no npm distribution`,
|
|
228
|
+
{ gate: 'npm-entry-closure', unitId: unit.id },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
await evidence.append({
|
|
232
|
+
phase: 'safety-gate',
|
|
233
|
+
gate: 'npm-entry-closure',
|
|
234
|
+
unitId: unit.id,
|
|
235
|
+
status: 'started',
|
|
236
|
+
});
|
|
237
|
+
await verifyFrozenNpmTarballContract({
|
|
238
|
+
package: npmDistribution.package,
|
|
239
|
+
version: unit.targetVersion,
|
|
240
|
+
tarballPath: frozen.npm.tarballPath,
|
|
241
|
+
tarballSha256: frozen.npm.tarballSha256,
|
|
242
|
+
integrity: frozen.npm.integrity,
|
|
243
|
+
}, root);
|
|
244
|
+
await evidence.append({
|
|
245
|
+
phase: 'safety-gate',
|
|
246
|
+
gate: 'npm-entry-closure',
|
|
247
|
+
unitId: unit.id,
|
|
248
|
+
status: 'passed',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
212
252
|
// =======================================================================
|
|
213
253
|
// Safety Gate 3: Load and validate source run
|
|
214
254
|
// =======================================================================
|
|
@@ -233,6 +273,18 @@ export async function reconcileRelease(options) {
|
|
|
233
273
|
`reconcile source command must be publish or reconcile, got "${sourceRun.command}"`,
|
|
234
274
|
);
|
|
235
275
|
}
|
|
276
|
+
let sourceAuthorityReceipt = null;
|
|
277
|
+
if (plan.sourceAuthority) {
|
|
278
|
+
const receiptResult = verifySourceAuthorityReceipt({ plan, run: sourceRun });
|
|
279
|
+
if (!receiptResult.passed) {
|
|
280
|
+
throw new ReleaseError(
|
|
281
|
+
GATE_FAILED,
|
|
282
|
+
`reconcile source run has no valid source-authority receipt: ${receiptResult.reason}`,
|
|
283
|
+
{ gate: 'source-authority', sourceRunId: sourceRun.runId },
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
sourceAuthorityReceipt = receiptResult.receipt;
|
|
287
|
+
}
|
|
236
288
|
|
|
237
289
|
let consumedApprovalPath = sourceRun.approvalPath;
|
|
238
290
|
let consumedApprovalDigest = sourceRun.approvalDigest;
|
|
@@ -956,6 +1008,9 @@ export async function reconcileRelease(options) {
|
|
|
956
1008
|
sourceRunDigest: sourceAuthorityDigest,
|
|
957
1009
|
sourceRunPath,
|
|
958
1010
|
status,
|
|
1011
|
+
...(sourceAuthorityReceipt
|
|
1012
|
+
? { sourceAuthorityReceipts: [sourceAuthorityReceipt] }
|
|
1013
|
+
: {}),
|
|
959
1014
|
checkpoints: planActions.map((action) => {
|
|
960
1015
|
const value = actionResults.get(action.id);
|
|
961
1016
|
const normalized = value === 'deferred' ? 'deferred'
|
|
@@ -1303,6 +1358,9 @@ export async function reconcileRelease(options) {
|
|
|
1303
1358
|
sourceRunDigest,
|
|
1304
1359
|
sourceRunPath,
|
|
1305
1360
|
status: overallStatus,
|
|
1361
|
+
...(sourceAuthorityReceipt
|
|
1362
|
+
? { sourceAuthorityReceipts: [sourceAuthorityReceipt] }
|
|
1363
|
+
: {}),
|
|
1306
1364
|
checkpoints: planActions.map((a) => {
|
|
1307
1365
|
const status = actionResults.get(a.id) ?? 'pending';
|
|
1308
1366
|
const normalized = status === 'deferred' ? 'deferred'
|