release-skill 0.9.3 → 0.9.4
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 +29 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +47 -15
- package/README.zh-CN.md +42 -15
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/consumer-contract-vectors.json +7 -7
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +651 -190
- package/adapters/claude/skills/release-help/SKILL.md +10 -3
- package/adapters/claude/skills/release-prepare/SKILL.md +11 -3
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/consumer-contract-vectors.json +7 -7
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +651 -190
- package/adapters/codex/skills/release-help/SKILL.md +10 -3
- package/adapters/codex/skills/release-prepare/SKILL.md +11 -3
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/consumer-contract-vectors.json +7 -7
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +651 -190
- package/adapters/kimi/skills/release-help/SKILL.md +10 -3
- package/adapters/kimi/skills/release-prepare/SKILL.md +11 -3
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/consumer-contract-vectors.json +7 -7
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +651 -190
- package/adapters/workbuddy/skills/release-help/SKILL.md +10 -3
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +11 -3
- package/bin/consumer-contract-vectors.json +7 -7
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/release-skill-cli.mjs +26 -2
- package/bin/release-skill.bundle.mjs +651 -190
- package/package.json +4 -4
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +4 -4
- package/references/01-state-machine.md +18 -0
- package/references/02-project-config.md +20 -0
- package/skills/release-help/SKILL.md +10 -3
- package/skills/release-prepare/SKILL.md +11 -3
- package/skills-src/release-help/SKILL.md +10 -3
- package/skills-src/release-prepare/SKILL.md +11 -3
- package/src/commands/post-release-local.mjs +169 -16
- package/src/commands/prepare.mjs +183 -7
- package/src/commands/ship.mjs +164 -2
- package/src/core/foundation-plugin-verification.mjs +22 -2
- package/src/producers/foundation-resource-projection.mjs +3 -3
package/src/commands/prepare.mjs
CHANGED
|
@@ -2917,6 +2917,142 @@ async function runPreHookPublicSurfaceGate({
|
|
|
2917
2917
|
}
|
|
2918
2918
|
}
|
|
2919
2919
|
|
|
2920
|
+
/**
|
|
2921
|
+
* Resolve the operator's pre-freeze release-unit request against the complete
|
|
2922
|
+
* validated project configuration. The returned units always follow config
|
|
2923
|
+
* declaration order so plan ordering and digests never depend on argv order.
|
|
2924
|
+
*/
|
|
2925
|
+
function prepareRetryPathArg(path) {
|
|
2926
|
+
return relative(process.cwd(), resolve(path)) || '.';
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
function buildPrepareRetryArgv(options, unitIds) {
|
|
2930
|
+
const argv = [
|
|
2931
|
+
'release-skill',
|
|
2932
|
+
'prepare',
|
|
2933
|
+
'--root',
|
|
2934
|
+
prepareRetryPathArg(options.root),
|
|
2935
|
+
];
|
|
2936
|
+
if (typeof options.version === 'string' && options.version.length > 0) {
|
|
2937
|
+
argv.push('--target-version', options.version);
|
|
2938
|
+
}
|
|
2939
|
+
argv.push(options.offline ? '--offline' : '--online');
|
|
2940
|
+
if (options.production) argv.push('--production');
|
|
2941
|
+
argv.push('--workflow', options.workflow);
|
|
2942
|
+
if (typeof options.testSelection === 'string') {
|
|
2943
|
+
argv.push('--test-selection', options.testSelection);
|
|
2944
|
+
}
|
|
2945
|
+
if (typeof options.output === 'string') {
|
|
2946
|
+
argv.push('--output', prepareRetryPathArg(options.output));
|
|
2947
|
+
}
|
|
2948
|
+
// A production --run-dir is single-use by contract and this failed attempt
|
|
2949
|
+
// has already created it. Let the retry allocate a fresh evidence directory.
|
|
2950
|
+
if (options.hookCache === false) argv.push('--no-hook-cache');
|
|
2951
|
+
for (const unitId of unitIds) argv.push('--unit', unitId);
|
|
2952
|
+
return argv;
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
/**
|
|
2956
|
+
* Keep the deferred-unit evidence and operator guidance on one exact argv.
|
|
2957
|
+
* The command points back to the caller's project root and carries the same
|
|
2958
|
+
* prepare mode while selecting only the units that were deferred.
|
|
2959
|
+
*/
|
|
2960
|
+
function buildDeferredReleaseDiagnostics(options, deferredUnitIds) {
|
|
2961
|
+
if (deferredUnitIds.length === 0) return null;
|
|
2962
|
+
return {
|
|
2963
|
+
deferredUnitIds: [...deferredUnitIds],
|
|
2964
|
+
nextPrepareArgv: buildPrepareRetryArgv(options, deferredUnitIds),
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
function resolveRequestedReleaseUnits(config, requestedUnitIds, retryOptions) {
|
|
2969
|
+
const availableUnits = config.releaseUnits ?? [];
|
|
2970
|
+
const availableUnitIds = availableUnits.map((unit) => unit.id);
|
|
2971
|
+
if (requestedUnitIds === undefined) {
|
|
2972
|
+
return {
|
|
2973
|
+
explicit: false,
|
|
2974
|
+
selectedUnits: availableUnits,
|
|
2975
|
+
selectedUnitIds: availableUnitIds,
|
|
2976
|
+
deferredUnitIds: [],
|
|
2977
|
+
receiptSelected: Boolean(config.publicSourceAuthorityReceipt),
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
if (!Array.isArray(requestedUnitIds) || requestedUnitIds.length === 0) {
|
|
2981
|
+
throw new ReleaseError(
|
|
2982
|
+
CONFIG_INVALID,
|
|
2983
|
+
'explicit release-unit selection must contain at least one unit id',
|
|
2984
|
+
{ requestedUnitIds, availableUnitIds },
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
const invalidUnitIds = requestedUnitIds.filter(
|
|
2989
|
+
(unitId) => typeof unitId !== 'string' || unitId.length === 0,
|
|
2990
|
+
);
|
|
2991
|
+
const seen = new Set();
|
|
2992
|
+
const duplicateUnitIds = [];
|
|
2993
|
+
for (const unitId of requestedUnitIds) {
|
|
2994
|
+
if (seen.has(unitId) && !duplicateUnitIds.includes(unitId)) duplicateUnitIds.push(unitId);
|
|
2995
|
+
seen.add(unitId);
|
|
2996
|
+
}
|
|
2997
|
+
const availableSet = new Set(availableUnitIds);
|
|
2998
|
+
const unknownUnitIds = requestedUnitIds.filter(
|
|
2999
|
+
(unitId) => typeof unitId === 'string' && unitId.length > 0 && !availableSet.has(unitId),
|
|
3000
|
+
);
|
|
3001
|
+
if (invalidUnitIds.length > 0 || duplicateUnitIds.length > 0 || unknownUnitIds.length > 0) {
|
|
3002
|
+
throw new ReleaseError(
|
|
3003
|
+
CONFIG_INVALID,
|
|
3004
|
+
'release-unit selection contains empty, duplicate, or unknown unit ids',
|
|
3005
|
+
{
|
|
3006
|
+
requestedUnitIds,
|
|
3007
|
+
availableUnitIds,
|
|
3008
|
+
invalidUnitIds,
|
|
3009
|
+
duplicateUnitIds,
|
|
3010
|
+
unknownUnitIds,
|
|
3011
|
+
},
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
const selectedSet = new Set(requestedUnitIds);
|
|
3016
|
+
const selectedUnits = availableUnits.filter((unit) => selectedSet.has(unit.id));
|
|
3017
|
+
const selectedUnitIds = selectedUnits.map((unit) => unit.id);
|
|
3018
|
+
const deferredUnitIds = availableUnitIds.filter((unitId) => !selectedSet.has(unitId));
|
|
3019
|
+
const deferredReleaseDiagnostics = buildDeferredReleaseDiagnostics(retryOptions, deferredUnitIds);
|
|
3020
|
+
const receipt = config.publicSourceAuthorityReceipt;
|
|
3021
|
+
let receiptSelected = false;
|
|
3022
|
+
if (receipt) {
|
|
3023
|
+
const receiptSet = new Set([receipt.coordinatorUnitId, ...receipt.subjectUnitIds]);
|
|
3024
|
+
const dependencyUnitIds = availableUnitIds.filter((unitId) => receiptSet.has(unitId));
|
|
3025
|
+
receiptSelected = dependencyUnitIds.some((unitId) => selectedSet.has(unitId));
|
|
3026
|
+
if (receiptSelected) {
|
|
3027
|
+
const missingUnitIds = dependencyUnitIds.filter((unitId) => !selectedSet.has(unitId));
|
|
3028
|
+
if (missingUnitIds.length > 0) {
|
|
3029
|
+
const retryUnitIds = availableUnitIds.filter(
|
|
3030
|
+
(unitId) => selectedSet.has(unitId) || receiptSet.has(unitId),
|
|
3031
|
+
);
|
|
3032
|
+
throw new ReleaseError(
|
|
3033
|
+
GATE_FAILED,
|
|
3034
|
+
'release-unit selection does not include the complete public source-authority dependency closure',
|
|
3035
|
+
{
|
|
3036
|
+
selectedUnitIds,
|
|
3037
|
+
missingUnitIds,
|
|
3038
|
+
dependencyUnitIds,
|
|
3039
|
+
retryArgv: buildPrepareRetryArgv(retryOptions, retryUnitIds),
|
|
3040
|
+
},
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
return {
|
|
3047
|
+
explicit: true,
|
|
3048
|
+
selectedUnits,
|
|
3049
|
+
selectedUnitIds,
|
|
3050
|
+
deferredUnitIds,
|
|
3051
|
+
deferredReleaseDiagnostics,
|
|
3052
|
+
receiptSelected,
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
|
|
2920
3056
|
export async function prepareRelease(options) {
|
|
2921
3057
|
const {
|
|
2922
3058
|
root,
|
|
@@ -2931,6 +3067,7 @@ export async function prepareRelease(options) {
|
|
|
2931
3067
|
workflow = 'full',
|
|
2932
3068
|
observePreviousPublicBaselineFn,
|
|
2933
3069
|
testSelection = 'full',
|
|
3070
|
+
unitIds,
|
|
2934
3071
|
} = options ?? {};
|
|
2935
3072
|
|
|
2936
3073
|
// --- Workflow profile (H5) ---
|
|
@@ -3039,7 +3176,20 @@ export async function prepareRelease(options) {
|
|
|
3039
3176
|
await evidence.append({ phase: 'config', status: 'started' });
|
|
3040
3177
|
|
|
3041
3178
|
const { config, configPath, configDigest } = await loadProjectConfig({ root: realRoot });
|
|
3042
|
-
const
|
|
3179
|
+
const releaseSelection = resolveRequestedReleaseUnits(config, unitIds, {
|
|
3180
|
+
root,
|
|
3181
|
+
version,
|
|
3182
|
+
offline,
|
|
3183
|
+
production,
|
|
3184
|
+
workflow,
|
|
3185
|
+
testSelection: typeof options?.testSelection === 'string' ? testSelection : undefined,
|
|
3186
|
+
output,
|
|
3187
|
+
hookCache: options?.hookCache,
|
|
3188
|
+
});
|
|
3189
|
+
const configUnits = releaseSelection.selectedUnits;
|
|
3190
|
+
const selectedConfig = { ...config, releaseUnits: configUnits };
|
|
3191
|
+
const selectedUnitIdSet = new Set(releaseSelection.selectedUnitIds);
|
|
3192
|
+
const adoptionWarnings = collectExpectedPublicSurfaceAdoptionWarnings(selectedConfig);
|
|
3043
3193
|
// Mutable operator-facing warning list: seeded from the adoption warnings,
|
|
3044
3194
|
// appended by later gates (O5 origin-ahead). Returned as `warnings`.
|
|
3045
3195
|
const runWarnings = [...adoptionWarnings];
|
|
@@ -3057,6 +3207,18 @@ export async function prepareRelease(options) {
|
|
|
3057
3207
|
...warning,
|
|
3058
3208
|
});
|
|
3059
3209
|
}
|
|
3210
|
+
if (releaseSelection.explicit) {
|
|
3211
|
+
await evidence.append({
|
|
3212
|
+
phase: 'release-scope',
|
|
3213
|
+
status: 'selected',
|
|
3214
|
+
mode: 'selected',
|
|
3215
|
+
selectedUnitIds: releaseSelection.selectedUnitIds,
|
|
3216
|
+
deferredUnitIds: releaseSelection.deferredUnitIds,
|
|
3217
|
+
...(releaseSelection.deferredReleaseDiagnostics
|
|
3218
|
+
? { nextPrepareArgv: releaseSelection.deferredReleaseDiagnostics.nextPrepareArgv }
|
|
3219
|
+
: {}),
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3060
3222
|
|
|
3061
3223
|
// --- Step 1-fresh: Bundle freshness gate (BUNDLE_STALE, fail-closed) ---
|
|
3062
3224
|
// 2026-08-18 investigation §4.2: a stale bin/release-skill.bundle.mjs
|
|
@@ -3132,7 +3294,6 @@ export async function prepareRelease(options) {
|
|
|
3132
3294
|
// gate, baseline, snapshot, remote check, or plan write runs. Units
|
|
3133
3295
|
// without releaseDocuments keep the exact legacy behaviour (the gate
|
|
3134
3296
|
// appends no evidence and performs no check).
|
|
3135
|
-
const configUnits = config.releaseUnits ?? [];
|
|
3136
3297
|
const resolvedVersions = await resolveAllUnitVersions(
|
|
3137
3298
|
configUnits,
|
|
3138
3299
|
realRoot,
|
|
@@ -3330,7 +3491,8 @@ export async function prepareRelease(options) {
|
|
|
3330
3491
|
});
|
|
3331
3492
|
}
|
|
3332
3493
|
|
|
3333
|
-
const declaredVerificationGates = config.verificationGates ?? []
|
|
3494
|
+
const declaredVerificationGates = (config.verificationGates ?? [])
|
|
3495
|
+
.filter((gate) => selectedUnitIdSet.has(gate.scope.unit));
|
|
3334
3496
|
if (declaredVerificationGates.length > 0) {
|
|
3335
3497
|
await evidence.append({
|
|
3336
3498
|
phase: 'verification-gate-authorization',
|
|
@@ -3435,7 +3597,7 @@ export async function prepareRelease(options) {
|
|
|
3435
3597
|
// R93-01: optional ephemeral public-surface check immediately before the
|
|
3436
3598
|
// first hook. Its staging and closure receipt never enter the plan.
|
|
3437
3599
|
await runPreHookPublicSurfaceGate({
|
|
3438
|
-
config,
|
|
3600
|
+
config: selectedConfig,
|
|
3439
3601
|
root: realRoot,
|
|
3440
3602
|
resolvedVersions,
|
|
3441
3603
|
evidence,
|
|
@@ -3894,7 +4056,7 @@ export async function prepareRelease(options) {
|
|
|
3894
4056
|
|
|
3895
4057
|
// --- Step 5: Build snapshots, scan, and evaluate README ---
|
|
3896
4058
|
const { unitResults, snapshotDigests } = await processSnapshots(
|
|
3897
|
-
|
|
4059
|
+
selectedConfig, realRoot, evidence, runDir, production,
|
|
3898
4060
|
);
|
|
3899
4061
|
|
|
3900
4062
|
// Snapshot gates always run on disposable writable copies. The public
|
|
@@ -4280,7 +4442,7 @@ export async function prepareRelease(options) {
|
|
|
4280
4442
|
}
|
|
4281
4443
|
|
|
4282
4444
|
let publicSourceAuthorityReceipt = null;
|
|
4283
|
-
if (config.publicSourceAuthorityReceipt) {
|
|
4445
|
+
if (config.publicSourceAuthorityReceipt && releaseSelection.receiptSelected) {
|
|
4284
4446
|
// P3 (ruling 7): config scenario A (no-publish-needed) means the public
|
|
4285
4447
|
// bytes are unchanged and nothing will be published — a configured
|
|
4286
4448
|
// public source receipt must not require or generate a nonexistent
|
|
@@ -4524,7 +4686,7 @@ export async function prepareRelease(options) {
|
|
|
4524
4686
|
},
|
|
4525
4687
|
}
|
|
4526
4688
|
: {}),
|
|
4527
|
-
verificationGates:
|
|
4689
|
+
verificationGates: declaredVerificationGates,
|
|
4528
4690
|
snapshotDigest: overallSnapshotDigest,
|
|
4529
4691
|
...(humanConsumersStrategy ? { humanConsumersStrategy } : {}),
|
|
4530
4692
|
...(production ? {
|
|
@@ -4608,6 +4770,13 @@ export async function prepareRelease(options) {
|
|
|
4608
4770
|
// carry no such warning. The prepare command's own defaults are unchanged —
|
|
4609
4771
|
// this only enriches the result, never flips offline/production.
|
|
4610
4772
|
const nextSteps = [];
|
|
4773
|
+
if (releaseSelection.explicit && releaseSelection.deferredUnitIds.length > 0) {
|
|
4774
|
+
nextSteps.push({
|
|
4775
|
+
code: 'DEFERRED_RELEASE_UNITS',
|
|
4776
|
+
message: `Prepare the deferred release units separately: ${releaseSelection.deferredUnitIds.join(', ')}`,
|
|
4777
|
+
argv: releaseSelection.deferredReleaseDiagnostics.nextPrepareArgv,
|
|
4778
|
+
});
|
|
4779
|
+
}
|
|
4611
4780
|
if (!production) {
|
|
4612
4781
|
nextSteps.push({
|
|
4613
4782
|
code: 'NON_PRODUCTION_PLAN_NOT_PUBLISHABLE',
|
|
@@ -4624,6 +4793,13 @@ export async function prepareRelease(options) {
|
|
|
4624
4793
|
evidenceDir,
|
|
4625
4794
|
warnings: runWarnings,
|
|
4626
4795
|
nextSteps,
|
|
4796
|
+
...(releaseSelection.explicit ? {
|
|
4797
|
+
releaseScope: {
|
|
4798
|
+
mode: 'selected',
|
|
4799
|
+
selectedUnitIds: releaseSelection.selectedUnitIds,
|
|
4800
|
+
deferredUnitIds: releaseSelection.deferredUnitIds,
|
|
4801
|
+
},
|
|
4802
|
+
} : {}),
|
|
4627
4803
|
};
|
|
4628
4804
|
} catch (err) {
|
|
4629
4805
|
// Record failure evidence (best effort). A broken clock can make the
|
package/src/commands/ship.mjs
CHANGED
|
@@ -15,11 +15,92 @@ import {
|
|
|
15
15
|
} from './post-release-local.mjs';
|
|
16
16
|
import {
|
|
17
17
|
ReleaseError,
|
|
18
|
+
CONFIG_INVALID,
|
|
18
19
|
GATE_FAILED,
|
|
19
20
|
PLAN_DIGEST_MISMATCH,
|
|
20
21
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
21
22
|
} from '../core/errors.mjs';
|
|
22
23
|
|
|
24
|
+
function normalizeNewShipUnitIds(releaseUnits, requestedUnitIds) {
|
|
25
|
+
if (requestedUnitIds === undefined) return undefined;
|
|
26
|
+
const availableUnitIds = (releaseUnits ?? []).map((unit) => unit.id);
|
|
27
|
+
if (!Array.isArray(requestedUnitIds) || requestedUnitIds.length === 0) {
|
|
28
|
+
throw new ReleaseError(
|
|
29
|
+
CONFIG_INVALID,
|
|
30
|
+
'explicit ship release-unit selection must contain at least one unit id',
|
|
31
|
+
{ requestedUnitIds, availableUnitIds },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const requestedSet = new Set(requestedUnitIds);
|
|
35
|
+
const invalid = requestedUnitIds.filter((unitId) => typeof unitId !== 'string' || unitId.length === 0);
|
|
36
|
+
const unknown = requestedUnitIds.filter((unitId) => !availableUnitIds.includes(unitId));
|
|
37
|
+
if (invalid.length > 0 || requestedSet.size !== requestedUnitIds.length || unknown.length > 0) {
|
|
38
|
+
throw new ReleaseError(
|
|
39
|
+
CONFIG_INVALID,
|
|
40
|
+
'ship release-unit selection contains empty, duplicate, or unknown unit ids',
|
|
41
|
+
{ requestedUnitIds, availableUnitIds },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return availableUnitIds.filter((unitId) => requestedSet.has(unitId));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sameUnitSelection(left, right) {
|
|
48
|
+
return Array.isArray(left)
|
|
49
|
+
&& Array.isArray(right)
|
|
50
|
+
&& left.length === right.length
|
|
51
|
+
&& new Set(left).size === left.length
|
|
52
|
+
&& new Set(right).size === right.length
|
|
53
|
+
&& left.every((unitId) => right.includes(unitId));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sameUnitOrder(left, right) {
|
|
57
|
+
return Array.isArray(left)
|
|
58
|
+
&& Array.isArray(right)
|
|
59
|
+
&& left.length === right.length
|
|
60
|
+
&& left.every((unitId, index) => unitId === right[index]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function unitScopeMismatch(statePath, stateUnitIds, requestedUnitIds, message) {
|
|
64
|
+
return new ReleaseError(
|
|
65
|
+
GATE_FAILED,
|
|
66
|
+
message,
|
|
67
|
+
{
|
|
68
|
+
reason: 'SHIP_STATE_UNIT_SCOPE_MISMATCH',
|
|
69
|
+
statePath,
|
|
70
|
+
stateUnitIds: stateUnitIds ?? null,
|
|
71
|
+
requestedUnitIds: requestedUnitIds ?? null,
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertFrozenPlanUnitScope({ statePath, stateUnitIds, expectedUnitIds, plan }) {
|
|
77
|
+
const planUnitIds = (plan.units ?? []).map((unit) => unit.id);
|
|
78
|
+
const exactMatch = Array.isArray(expectedUnitIds)
|
|
79
|
+
&& planUnitIds.length === expectedUnitIds.length
|
|
80
|
+
&& planUnitIds.every((unitId, index) => unitId === expectedUnitIds[index]);
|
|
81
|
+
if (!exactMatch) {
|
|
82
|
+
throw unitScopeMismatch(
|
|
83
|
+
statePath,
|
|
84
|
+
stateUnitIds,
|
|
85
|
+
planUnitIds,
|
|
86
|
+
'ship state release-unit scope does not match frozen plan.units',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readFrozenPlanForUnitScope(planPath, statePath, stateUnitIds) {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(await readFile(planPath, 'utf8'));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw unitScopeMismatch(
|
|
96
|
+
statePath,
|
|
97
|
+
stateUnitIds,
|
|
98
|
+
null,
|
|
99
|
+
`cannot verify ship state release-unit scope against frozen plan: ${error.message}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
23
104
|
async function writeJsonAtomic(path, value) {
|
|
24
105
|
await mkdir(dirname(path), { recursive: true });
|
|
25
106
|
const { stateDigest: _oldDigest, ...body } = value;
|
|
@@ -102,6 +183,7 @@ function publicState(state) {
|
|
|
102
183
|
...(state.hooks && state.hooks.length > 0 ? {
|
|
103
184
|
hooks: state.hooks,
|
|
104
185
|
} : {}),
|
|
186
|
+
...(state.selectedUnitIds ? { selectedUnitIds: state.selectedUnitIds } : {}),
|
|
105
187
|
...(state.planPath ? {
|
|
106
188
|
planPath: state.planPath,
|
|
107
189
|
planDigest: state.planDigest,
|
|
@@ -334,6 +416,24 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
334
416
|
);
|
|
335
417
|
}
|
|
336
418
|
}
|
|
419
|
+
if (state && options.unitIds !== undefined) {
|
|
420
|
+
if (!Array.isArray(state.selectedUnitIds)) {
|
|
421
|
+
throw unitScopeMismatch(
|
|
422
|
+
statePath,
|
|
423
|
+
null,
|
|
424
|
+
options.unitIds,
|
|
425
|
+
'an existing full-scope or legacy ship state cannot acquire --unit; provide a new --state path',
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
if (!sameUnitSelection(state.selectedUnitIds, options.unitIds)) {
|
|
429
|
+
throw unitScopeMismatch(
|
|
430
|
+
statePath,
|
|
431
|
+
state.selectedUnitIds,
|
|
432
|
+
options.unitIds,
|
|
433
|
+
'ship state release-unit scope does not match the requested --unit values',
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
337
437
|
if (state?.gitTransport) {
|
|
338
438
|
process.env.RELEASE_SKILL_GIT_TRANSPORT = state.gitTransport;
|
|
339
439
|
}
|
|
@@ -341,6 +441,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
341
441
|
if (!state) {
|
|
342
442
|
const loaded = await deps.loadProjectConfig({ root });
|
|
343
443
|
const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
|
|
444
|
+
const selectedUnitIds = normalizeNewShipUnitIds(loaded.config.releaseUnits, options.unitIds);
|
|
344
445
|
state = {
|
|
345
446
|
schemaVersion: 2,
|
|
346
447
|
root,
|
|
@@ -348,6 +449,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
348
449
|
targetVersion: options.targetVersion ?? null,
|
|
349
450
|
configDigest: loaded.configDigest,
|
|
350
451
|
hooks,
|
|
452
|
+
...(selectedUnitIds ? { selectedUnitIds } : {}),
|
|
351
453
|
status: 'NEW',
|
|
352
454
|
createdAt: new Date().toISOString(),
|
|
353
455
|
updatedAt: new Date().toISOString(),
|
|
@@ -364,8 +466,53 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
364
466
|
await writeJsonAtomic(statePath, state);
|
|
365
467
|
}
|
|
366
468
|
|
|
469
|
+
if (state.planPath && Array.isArray(state.selectedUnitIds)) {
|
|
470
|
+
const frozenPlan = await readFrozenPlanForUnitScope(
|
|
471
|
+
state.planPath,
|
|
472
|
+
statePath,
|
|
473
|
+
state.selectedUnitIds,
|
|
474
|
+
);
|
|
475
|
+
assertFrozenPlanUnitScope({
|
|
476
|
+
statePath,
|
|
477
|
+
stateUnitIds: state.selectedUnitIds,
|
|
478
|
+
expectedUnitIds: state.selectedUnitIds,
|
|
479
|
+
plan: frozenPlan,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
367
483
|
if (state.status === 'NEW') {
|
|
368
484
|
const loaded = await deps.loadProjectConfig({ root });
|
|
485
|
+
let stateChanged = false;
|
|
486
|
+
if (Array.isArray(state.selectedUnitIds)) {
|
|
487
|
+
const normalizedStateUnitIds = normalizeNewShipUnitIds(
|
|
488
|
+
loaded.config.releaseUnits,
|
|
489
|
+
state.selectedUnitIds,
|
|
490
|
+
);
|
|
491
|
+
if (!sameUnitSelection(normalizedStateUnitIds, state.selectedUnitIds)) {
|
|
492
|
+
throw unitScopeMismatch(
|
|
493
|
+
statePath,
|
|
494
|
+
state.selectedUnitIds,
|
|
495
|
+
normalizedStateUnitIds,
|
|
496
|
+
'ship state release-unit scope no longer matches project configuration',
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
if (!sameUnitOrder(normalizedStateUnitIds, state.selectedUnitIds)) {
|
|
500
|
+
if (state.planPath) {
|
|
501
|
+
throw unitScopeMismatch(
|
|
502
|
+
statePath,
|
|
503
|
+
state.selectedUnitIds,
|
|
504
|
+
normalizedStateUnitIds,
|
|
505
|
+
'frozen ship state release-unit order no longer matches project configuration',
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
state = {
|
|
509
|
+
...state,
|
|
510
|
+
selectedUnitIds: normalizedStateUnitIds,
|
|
511
|
+
updatedAt: new Date().toISOString(),
|
|
512
|
+
};
|
|
513
|
+
stateChanged = true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
369
516
|
if (loaded.configDigest !== state.configDigest) {
|
|
370
517
|
const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
|
|
371
518
|
state = {
|
|
@@ -375,8 +522,9 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
375
522
|
status: 'NEW',
|
|
376
523
|
updatedAt: new Date().toISOString(),
|
|
377
524
|
};
|
|
378
|
-
|
|
525
|
+
stateChanged = true;
|
|
379
526
|
}
|
|
527
|
+
if (stateChanged) await writeJsonAtomic(statePath, state);
|
|
380
528
|
const prepared = await deps.prepareRelease({
|
|
381
529
|
root,
|
|
382
530
|
version: state.targetVersion ?? undefined,
|
|
@@ -385,10 +533,24 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
385
533
|
hooksAuthorized: true,
|
|
386
534
|
verificationGatesAuthorized: true,
|
|
387
535
|
hookCache: true,
|
|
536
|
+
...(state.selectedUnitIds ? { unitIds: state.selectedUnitIds } : {}),
|
|
388
537
|
});
|
|
538
|
+
let frozenPlan = null;
|
|
539
|
+
if (Array.isArray(state.selectedUnitIds) || deps.preflightGitTransports) {
|
|
540
|
+
frozenPlan = Array.isArray(state.selectedUnitIds)
|
|
541
|
+
? await readFrozenPlanForUnitScope(prepared.planPath, statePath, state.selectedUnitIds)
|
|
542
|
+
: JSON.parse(await readFile(prepared.planPath, 'utf8'));
|
|
543
|
+
}
|
|
544
|
+
if (Array.isArray(state.selectedUnitIds)) {
|
|
545
|
+
assertFrozenPlanUnitScope({
|
|
546
|
+
statePath,
|
|
547
|
+
stateUnitIds: state.selectedUnitIds,
|
|
548
|
+
expectedUnitIds: state.selectedUnitIds,
|
|
549
|
+
plan: frozenPlan,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
389
552
|
let transportPreflight = null;
|
|
390
553
|
if (deps.preflightGitTransports) {
|
|
391
|
-
const frozenPlan = JSON.parse(await readFile(prepared.planPath, 'utf8'));
|
|
392
554
|
transportPreflight = await deps.preflightGitTransports(frozenPlan);
|
|
393
555
|
process.env.RELEASE_SKILL_GIT_TRANSPORT = transportPreflight.transport;
|
|
394
556
|
}
|
|
@@ -33,7 +33,6 @@ import { ReleaseError, POST_PUBLISH_VERIFY_FAILED } from './errors.mjs';
|
|
|
33
33
|
// this value so an upstream contract change fails closed during adoption.
|
|
34
34
|
const FOUNDATION_PLUGIN_DRIVER_VERSION = '1.0.0';
|
|
35
35
|
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
36
|
-
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
|
|
37
36
|
|
|
38
37
|
function isSafeRelativePath(value) {
|
|
39
38
|
return typeof value === 'string'
|
|
@@ -46,7 +45,28 @@ function isSafeRelativePath(value) {
|
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
function decodeCanonicalBase64(value) {
|
|
49
|
-
if (typeof value !== 'string' ||
|
|
48
|
+
if (typeof value !== 'string' || value.length % 4 !== 0) return null;
|
|
49
|
+
let paddingStart = -1;
|
|
50
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
51
|
+
const code = value.charCodeAt(index);
|
|
52
|
+
if (code === 61) {
|
|
53
|
+
if (paddingStart === -1) paddingStart = index;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (paddingStart !== -1 || !(
|
|
57
|
+
code >= 65 && code <= 90
|
|
58
|
+
|| code >= 97 && code <= 122
|
|
59
|
+
|| code >= 48 && code <= 57
|
|
60
|
+
|| code === 43
|
|
61
|
+
|| code === 47
|
|
62
|
+
)) return null;
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
paddingStart !== -1
|
|
66
|
+
&& value.length - paddingStart > 2
|
|
67
|
+
|| paddingStart !== -1
|
|
68
|
+
&& paddingStart % 4 < 2
|
|
69
|
+
) return null;
|
|
50
70
|
const decoded = Buffer.from(value, 'base64');
|
|
51
71
|
return decoded.toString('base64') === value ? decoded : null;
|
|
52
72
|
}
|
|
@@ -55,9 +55,9 @@ import { digestBytes, publishFileOrReplace, readFileStrict, withTemporaryWorkspa
|
|
|
55
55
|
import { canonicalJson } from '../core/digest.mjs';
|
|
56
56
|
|
|
57
57
|
export const FOUNDATION_PACKAGES = Object.freeze({
|
|
58
|
-
'skill-family-contracts': '0.
|
|
59
|
-
'skill-family-engineering-kit': '0.
|
|
60
|
-
'skill-family-harness-node': '0.
|
|
58
|
+
'skill-family-contracts': '0.16.0',
|
|
59
|
+
'skill-family-engineering-kit': '0.16.0',
|
|
60
|
+
'skill-family-harness-node': '0.16.0',
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
export const BINDING_RECORD_PATH = 'bin/foundation-resource-binding.json';
|