release-skill 0.9.4 → 0.9.6
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 +56 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +21 -17
- package/README.zh-CN.md +18 -16
- 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 +1036 -174
- package/adapters/claude/schemas/release-plan.schema.json +36 -0
- package/adapters/claude/schemas/release-project.schema.json +31 -0
- package/adapters/claude/skills/release-finish/SKILL.md +5 -5
- package/adapters/claude/skills/release-help/SKILL.md +7 -3
- package/adapters/claude/skills/release-verify/SKILL.md +14 -1
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1036 -174
- package/adapters/codex/schemas/release-plan.schema.json +36 -0
- package/adapters/codex/schemas/release-project.schema.json +31 -0
- package/adapters/codex/skills/release-finish/SKILL.md +5 -5
- package/adapters/codex/skills/release-help/SKILL.md +7 -3
- package/adapters/codex/skills/release-verify/SKILL.md +14 -1
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1036 -174
- package/adapters/kimi/schemas/release-plan.schema.json +36 -0
- package/adapters/kimi/schemas/release-project.schema.json +31 -0
- package/adapters/kimi/skills/release-finish/SKILL.md +5 -5
- package/adapters/kimi/skills/release-help/SKILL.md +7 -3
- package/adapters/kimi/skills/release-verify/SKILL.md +14 -1
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1036 -174
- package/adapters/workbuddy/schemas/release-plan.schema.json +36 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +31 -0
- package/adapters/workbuddy/skills/release-finish/SKILL.md +5 -5
- package/adapters/workbuddy/skills/release-help/SKILL.md +7 -3
- package/adapters/workbuddy/skills/release-verify/SKILL.md +14 -1
- package/bin/release-skill-cli.mjs +217 -26
- package/bin/release-skill.bundle.mjs +1036 -174
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/schemas/release-plan.schema.json +36 -0
- package/schemas/release-project.schema.json +31 -0
- package/skills/release-finish/SKILL.md +5 -5
- package/skills/release-help/SKILL.md +7 -3
- package/skills/release-verify/SKILL.md +14 -1
- package/skills-src/release-finish/SKILL.md +5 -5
- package/skills-src/release-help/SKILL.md +7 -3
- package/skills-src/release-verify/SKILL.md +14 -1
- package/src/adapters/plugin-marketplace.mjs +54 -5
- package/src/commands/post-release-local.mjs +204 -10
- package/src/commands/prepare.mjs +178 -9
- package/src/commands/ship.mjs +26 -11
- package/src/commands/verify.mjs +175 -2
- package/src/core/plan.mjs +45 -1
- package/src/platforms/registry.mjs +97 -0
- package/src/readme/contract.mjs +23 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access } from 'node:fs/promises';
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join, relative } from 'node:path';
|
|
4
4
|
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
import { getPlatform } from '../platforms/registry.mjs';
|
|
13
13
|
import { resolveCodeBuddyMarketplace } from '../platforms/codebuddy.mjs';
|
|
14
14
|
import { verifyInstalledMarketplacePayload } from '../adapters/plugin-marketplace.mjs';
|
|
15
|
+
import { normalizePostPublishView, postPublishActionId } from '../core/postpublish.mjs';
|
|
16
|
+
import { loadRun, validateRunLineage } from '../core/run.mjs';
|
|
15
17
|
|
|
16
18
|
const HOSTS_BY_ACTION = Object.freeze({
|
|
17
19
|
'claude-marketplace-install': ['claude'],
|
|
@@ -34,6 +36,7 @@ const SAFE_ENV_KEYS = Object.freeze([
|
|
|
34
36
|
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
|
|
35
37
|
'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
|
|
36
38
|
'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'KIMI_CONFIG_DIR',
|
|
39
|
+
'CODEBUDDY_CONFIG_DIR', 'WORKBUDDY_CONFIG_DIR',
|
|
37
40
|
]);
|
|
38
41
|
const CODEBUDDY_PLUGIN_LIST_ARGS = Object.freeze(['plugin', 'list', '--json']);
|
|
39
42
|
const CODEBUDDY_MARKETPLACE_LIST_ARGS = Object.freeze(['plugin', 'marketplace', 'list']);
|
|
@@ -169,7 +172,27 @@ function assertExecutableTarget(target) {
|
|
|
169
172
|
}
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
|
|
175
|
+
function buildShipNextStep({ root, statePath, unitIds }) {
|
|
176
|
+
const argv = ['release-skill', 'ship'];
|
|
177
|
+
if (typeof root === 'string' && root.length > 0) argv.push('--root', root);
|
|
178
|
+
if (typeof statePath === 'string' && statePath.length > 0) argv.push('--state', statePath);
|
|
179
|
+
if (Array.isArray(unitIds) && unitIds.length > 0) {
|
|
180
|
+
for (const unitId of unitIds) argv.push('--unit', unitId);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
code: 'COMPLETE_POST_VERIFY',
|
|
184
|
+
message: 'Complete the postVerify phase with ship before running post-release.',
|
|
185
|
+
argv,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function derivePostReleaseChecklist(plan, {
|
|
190
|
+
runPath,
|
|
191
|
+
root,
|
|
192
|
+
statePath,
|
|
193
|
+
unitIds,
|
|
194
|
+
postVerifyComplete = false,
|
|
195
|
+
} = {}) {
|
|
173
196
|
if (!plan || typeof plan !== 'object' || typeof plan.digest !== 'string') {
|
|
174
197
|
throw new Error('a frozen release plan with digest is required');
|
|
175
198
|
}
|
|
@@ -178,6 +201,9 @@ export function derivePostReleaseChecklist(plan) {
|
|
|
178
201
|
!BRANCH_ACTION_INCLUDED.has(unit.productionConfig?.branchStrategy)
|
|
179
202
|
));
|
|
180
203
|
const targets = pluginTargets(plan);
|
|
204
|
+
const hasPendingPostVerify = postVerifyHooks(plan).length > 0 && !postVerifyComplete && targets.length > 0;
|
|
205
|
+
const hasStatePath = typeof statePath === 'string' && statePath.length > 0;
|
|
206
|
+
const selectedUnitIds = Array.isArray(unitIds) ? unitIds : undefined;
|
|
181
207
|
return {
|
|
182
208
|
command: 'post-release',
|
|
183
209
|
status: 'AWAITING_USER_DECISION',
|
|
@@ -193,14 +219,173 @@ export function derivePostReleaseChecklist(plan) {
|
|
|
193
219
|
})),
|
|
194
220
|
},
|
|
195
221
|
localHostUpdate: {
|
|
196
|
-
promptRequired: targets.length > 0,
|
|
197
|
-
available: targets.length > 0,
|
|
222
|
+
promptRequired: targets.length > 0 && !hasPendingPostVerify,
|
|
223
|
+
available: targets.length > 0 && !hasPendingPostVerify,
|
|
224
|
+
...(!hasPendingPostVerify && runPath ? { runPath } : {}),
|
|
225
|
+
...(hasPendingPostVerify ? {
|
|
226
|
+
...(hasStatePath ? { nextSteps: [buildShipNextStep({ root, statePath, unitIds: selectedUnitIds })] } : {}),
|
|
227
|
+
} : {}),
|
|
198
228
|
hosts: [...new Set(targets.map((target) => target.host))].sort(),
|
|
199
229
|
targets,
|
|
200
230
|
},
|
|
201
231
|
};
|
|
202
232
|
}
|
|
203
233
|
|
|
234
|
+
function postVerifyHooks(plan) {
|
|
235
|
+
if (plan?.planVersion === undefined) return [];
|
|
236
|
+
return normalizePostPublishView(plan).flatMap((declaration) => (
|
|
237
|
+
(declaration.hooks ?? [])
|
|
238
|
+
.filter((hook) => hook.phase === 'postVerify')
|
|
239
|
+
.map((hook) => ({
|
|
240
|
+
actionId: postPublishActionId({ planVersion: plan.planVersion, unitId: declaration.unitId, localId: hook.id }),
|
|
241
|
+
hook,
|
|
242
|
+
unitId: declaration.unitId,
|
|
243
|
+
}))
|
|
244
|
+
));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function localFinishEvidenceError(message, { cause, root, statePath, unitIds } = {}) {
|
|
248
|
+
const error = new Error(`local host update evidence is not ready: ${message}; next step: obtain approval if required, then complete the postVerify phase with ship before rerunning post-release`);
|
|
249
|
+
error.code = 'LOCAL_FINISH_EVIDENCE_NOT_READY';
|
|
250
|
+
const hasStatePath = typeof statePath === 'string' && statePath.length > 0;
|
|
251
|
+
error.details = {
|
|
252
|
+
cause: {
|
|
253
|
+
code: cause?.code ?? 'LOCAL_FINISH_EVIDENCE_NOT_READY',
|
|
254
|
+
message: cause?.message ?? message,
|
|
255
|
+
},
|
|
256
|
+
...(hasStatePath ? {
|
|
257
|
+
nextSteps: [buildShipNextStep({ root, statePath, unitIds })],
|
|
258
|
+
} : {}),
|
|
259
|
+
};
|
|
260
|
+
return error;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Validate the frozen run authority before allowing any local host command.
|
|
265
|
+
* This is the only exported entry that can perform local host writes.
|
|
266
|
+
*/
|
|
267
|
+
export async function updateLocalHostPlugins({
|
|
268
|
+
plan: _suppliedPlan,
|
|
269
|
+
planPath,
|
|
270
|
+
runPath,
|
|
271
|
+
runRecord: _suppliedRunRecord,
|
|
272
|
+
production: _suppliedProduction,
|
|
273
|
+
root = process.cwd(),
|
|
274
|
+
statePath,
|
|
275
|
+
unitIds,
|
|
276
|
+
...options
|
|
277
|
+
} = {}) {
|
|
278
|
+
if (!planPath || !runPath) {
|
|
279
|
+
throw localFinishEvidenceError(
|
|
280
|
+
'planPath and runPath are required before local host updates',
|
|
281
|
+
{ root, statePath, unitIds },
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
let plan;
|
|
285
|
+
let runRecord;
|
|
286
|
+
try {
|
|
287
|
+
plan = JSON.parse(await readFile(planPath, 'utf8'));
|
|
288
|
+
runRecord = await loadRun(runPath, {
|
|
289
|
+
requireDigest: true,
|
|
290
|
+
authorityPlanPath: planPath,
|
|
291
|
+
});
|
|
292
|
+
} catch (cause) {
|
|
293
|
+
throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds });
|
|
294
|
+
}
|
|
295
|
+
await assertLocalFinishRun({
|
|
296
|
+
plan,
|
|
297
|
+
planPath,
|
|
298
|
+
runPath,
|
|
299
|
+
runRecord,
|
|
300
|
+
production: Boolean(plan.production),
|
|
301
|
+
root,
|
|
302
|
+
statePath,
|
|
303
|
+
unitIds,
|
|
304
|
+
});
|
|
305
|
+
return updateLocalHostPluginsInternal({
|
|
306
|
+
plan,
|
|
307
|
+
root,
|
|
308
|
+
...options,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Validate the explicit run supplied to local-finish before any host probe. */
|
|
313
|
+
export async function assertLocalFinishRun({
|
|
314
|
+
plan,
|
|
315
|
+
planPath,
|
|
316
|
+
runPath,
|
|
317
|
+
runRecord,
|
|
318
|
+
production = false,
|
|
319
|
+
root,
|
|
320
|
+
statePath,
|
|
321
|
+
unitIds,
|
|
322
|
+
} = {}) {
|
|
323
|
+
const hooks = postVerifyHooks(plan);
|
|
324
|
+
if (hooks.length === 0) {
|
|
325
|
+
try {
|
|
326
|
+
await validateRunLineage(runRecord, { plan, planPath, runPath, production });
|
|
327
|
+
assertVerifiedReleaseRun(plan, runRecord);
|
|
328
|
+
} catch (cause) {
|
|
329
|
+
throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds, plan });
|
|
330
|
+
}
|
|
331
|
+
return { runPath, phase: 'verify' };
|
|
332
|
+
}
|
|
333
|
+
if (runRecord?.command !== 'postverify' || runRecord?.status !== 'DISTRIBUTED') {
|
|
334
|
+
throw localFinishEvidenceError('the plan declares postVerify hooks, so the supplied verify run cannot authorize local-finish', { root, statePath, unitIds, plan });
|
|
335
|
+
}
|
|
336
|
+
if (runRecord.planDigest !== plan?.digest) {
|
|
337
|
+
throw localFinishEvidenceError('postVerify run is not bound to the frozen plan', { root, statePath, unitIds, plan });
|
|
338
|
+
}
|
|
339
|
+
if (typeof runRecord.sourceRunPath !== 'string' || typeof runRecord.sourceRunId !== 'string' || typeof runRecord.sourceRunDigest !== 'string') {
|
|
340
|
+
throw localFinishEvidenceError('completed postVerify run has incomplete verify-run lineage', { root, statePath, unitIds, plan });
|
|
341
|
+
}
|
|
342
|
+
const checkpoints = Array.isArray(runRecord.checkpoints) ? runRecord.checkpoints : null;
|
|
343
|
+
const expectedIds = hooks.map(({ actionId }) => actionId);
|
|
344
|
+
const actualIds = checkpoints?.map((checkpoint) => checkpoint?.actionId) ?? [];
|
|
345
|
+
if (
|
|
346
|
+
!checkpoints
|
|
347
|
+
|| actualIds.length !== expectedIds.length
|
|
348
|
+
|| new Set(actualIds).size !== actualIds.length
|
|
349
|
+
|| actualIds.some((id) => !expectedIds.includes(id))
|
|
350
|
+
|| expectedIds.some((id) => !actualIds.includes(id))
|
|
351
|
+
|| checkpoints.some((checkpoint) => (
|
|
352
|
+
checkpoint.actionType !== 'postpublish-hook'
|
|
353
|
+
|| !['succeeded', 'NO_CHANGE'].includes(checkpoint.status)
|
|
354
|
+
))
|
|
355
|
+
) {
|
|
356
|
+
throw localFinishEvidenceError('postVerify checkpoints must match each declared hook exactly and be succeeded or NO_CHANGE', { root, statePath, unitIds, plan });
|
|
357
|
+
}
|
|
358
|
+
let sourceRun;
|
|
359
|
+
try {
|
|
360
|
+
sourceRun = await loadRun(runRecord.sourceRunPath, {
|
|
361
|
+
requireDigest: true,
|
|
362
|
+
authorityPlanPath: planPath,
|
|
363
|
+
});
|
|
364
|
+
} catch (cause) {
|
|
365
|
+
throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds, plan });
|
|
366
|
+
}
|
|
367
|
+
if (
|
|
368
|
+
sourceRun.command !== 'verify'
|
|
369
|
+
|| sourceRun.status !== 'VERIFIED'
|
|
370
|
+
|| sourceRun.runId !== runRecord.sourceRunId
|
|
371
|
+
|| sourceRun.runDigest !== runRecord.sourceRunDigest
|
|
372
|
+
|| sourceRun.planDigest !== plan.digest
|
|
373
|
+
) {
|
|
374
|
+
throw localFinishEvidenceError('postVerify lineage does not point to the same-plan VERIFIED run', { root, statePath, unitIds, plan });
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
await validateRunLineage(sourceRun, {
|
|
378
|
+
plan,
|
|
379
|
+
planPath,
|
|
380
|
+
runPath: runRecord.sourceRunPath,
|
|
381
|
+
production,
|
|
382
|
+
});
|
|
383
|
+
} catch (cause) {
|
|
384
|
+
throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds, plan });
|
|
385
|
+
}
|
|
386
|
+
return { runPath, sourceRunPath: runRecord.sourceRunPath, phase: 'postverify' };
|
|
387
|
+
}
|
|
388
|
+
|
|
204
389
|
export function unavailablePostReleaseChecklist(plan, error) {
|
|
205
390
|
return {
|
|
206
391
|
command: 'post-release',
|
|
@@ -232,7 +417,11 @@ function hostEnvironment(host, { kimiHome } = {}) {
|
|
|
232
417
|
env.HOME ??= homedir();
|
|
233
418
|
env.PATH ??= '/usr/bin:/bin';
|
|
234
419
|
env.GIT_TERMINAL_PROMPT = '0';
|
|
235
|
-
if (host === '
|
|
420
|
+
if (host === 'codebuddy') env.CODEBUDDY_CONFIG_DIR = join(env.HOME, '.codebuddy');
|
|
421
|
+
if (host === 'workbuddy') {
|
|
422
|
+
env.CODEBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
|
|
423
|
+
env.WORKBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
|
|
424
|
+
}
|
|
236
425
|
if (host === 'kimi' && kimiHome) env.KIMI_CONFIG_DIR = kimiHome;
|
|
237
426
|
return env;
|
|
238
427
|
}
|
|
@@ -286,12 +475,17 @@ async function commandAvailable(command, host, run) {
|
|
|
286
475
|
}
|
|
287
476
|
|
|
288
477
|
async function defaultDetect(host, run = defaultRun) {
|
|
289
|
-
if (host === 'codebuddy'
|
|
290
|
-
for (const command of ['codebuddy', 'cbc'
|
|
478
|
+
if (host === 'codebuddy') {
|
|
479
|
+
for (const command of ['codebuddy', 'cbc']) {
|
|
291
480
|
if (await commandAvailable(command, host, run)) return { available: true, command };
|
|
292
481
|
}
|
|
293
482
|
return { available: false, reason: 'CodeBuddy/WorkBuddy CLI not found' };
|
|
294
483
|
}
|
|
484
|
+
if (host === 'workbuddy') {
|
|
485
|
+
if (process.platform !== 'darwin') return { available: false, status: 'SKIPPED_UNSUPPORTED_PLATFORM', reason: 'WorkBuddy local update is supported only on macOS' };
|
|
486
|
+
if (await commandAvailable(CODEBUDDY_MACOS_PATH, host, run)) return { available: true, command: CODEBUDDY_MACOS_PATH };
|
|
487
|
+
return { available: false, reason: 'WorkBuddy embedded CLI not found' };
|
|
488
|
+
}
|
|
295
489
|
if (host === 'kimi') {
|
|
296
490
|
if (!await commandAvailable('kimi', host, run)) return { available: false, reason: 'kimi CLI not found' };
|
|
297
491
|
try {
|
|
@@ -894,7 +1088,7 @@ function failedHostResult(target, error) {
|
|
|
894
1088
|
};
|
|
895
1089
|
}
|
|
896
1090
|
|
|
897
|
-
|
|
1091
|
+
async function updateLocalHostPluginsInternal({
|
|
898
1092
|
plan,
|
|
899
1093
|
root = process.cwd(),
|
|
900
1094
|
confirmPlanDigest,
|
|
@@ -905,7 +1099,7 @@ export async function updateLocalHostPlugins({
|
|
|
905
1099
|
verifyInstalledPayload = verifyInstalledMarketplacePayload,
|
|
906
1100
|
} = {}) {
|
|
907
1101
|
const effectiveKimiHome = kimiHome ?? process.env.KIMI_CONFIG_DIR ?? join(homedir(), '.kimi-code');
|
|
908
|
-
const checklist = derivePostReleaseChecklist(plan);
|
|
1102
|
+
const checklist = derivePostReleaseChecklist(plan, { postVerifyComplete: true });
|
|
909
1103
|
if (confirmPlanDigest !== plan.digest) {
|
|
910
1104
|
throw new Error('plan digest confirmation does not match the frozen release plan');
|
|
911
1105
|
}
|
|
@@ -923,7 +1117,7 @@ export async function updateLocalHostPlugins({
|
|
|
923
1117
|
results.push({
|
|
924
1118
|
host: target.host,
|
|
925
1119
|
unitId: target.unitId,
|
|
926
|
-
status: 'SKIPPED_NOT_INSTALLED',
|
|
1120
|
+
status: detected?.status ?? 'SKIPPED_NOT_INSTALLED',
|
|
927
1121
|
reason: detected?.reason ?? 'host unavailable',
|
|
928
1122
|
});
|
|
929
1123
|
continue;
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -77,7 +77,11 @@ import { acquireProjectLock } from '../artifacts/project-lock.mjs';
|
|
|
77
77
|
import { observePreviousPublicBaseline } from '../core/previous-public-baseline.mjs';
|
|
78
78
|
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
79
79
|
import { createProductionPrepareRunDir } from '../core/run.mjs';
|
|
80
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
PLATFORMS,
|
|
82
|
+
normalizeHostId,
|
|
83
|
+
buildExpectedStandaloneIndexInstallIdentity,
|
|
84
|
+
} from '../platforms/registry.mjs';
|
|
81
85
|
import { deriveSurfaceHostBinding, pluginRootFromManifestRelativePath } from '../core/surface-host-bindings.mjs';
|
|
82
86
|
import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
|
|
83
87
|
import { buildInstallationContract, computeInstallationContractDigest, INSTALLATION_CONTRACT_ALGORITHM_VERSION } from '../core/installation-contract.mjs';
|
|
@@ -97,6 +101,7 @@ import { digestReleaseAssetIdentities } from '../core/release-assets.mjs';
|
|
|
97
101
|
|
|
98
102
|
/** 消费端安装验证配方版本。算法变更时递增。 */
|
|
99
103
|
const CONSUMER_INSTALL_RECIPE_VERSION = 'consumer-install-v1';
|
|
104
|
+
const FIRST_RELEASE_BOOTSTRAP_MODE = 'manual-index-checkpoint';
|
|
100
105
|
|
|
101
106
|
// ---------------------------------------------------------------------------
|
|
102
107
|
// Version resolution
|
|
@@ -1498,6 +1503,72 @@ async function defaultFetchExternalMarketplaceIndex(repo, manifestPath, ref, { g
|
|
|
1498
1503
|
}
|
|
1499
1504
|
}
|
|
1500
1505
|
|
|
1506
|
+
/**
|
|
1507
|
+
* Observe the plugin repository before a first standalone-index release.
|
|
1508
|
+
* This is deliberately a read-only, fail-closed observation: an empty
|
|
1509
|
+
* repository, no target tag, and no target GitHub Release are all required.
|
|
1510
|
+
*/
|
|
1511
|
+
export function parseFirstReleasePluginRepositoryRefs(stdout, tag) {
|
|
1512
|
+
if (typeof stdout !== 'string' || typeof tag !== 'string' || tag.length === 0) {
|
|
1513
|
+
return { hasRefs: false, tagExists: false };
|
|
1514
|
+
}
|
|
1515
|
+
const refs = [];
|
|
1516
|
+
let hasRefs = false;
|
|
1517
|
+
for (const line of stdout.trim().split('\n').filter(Boolean)) {
|
|
1518
|
+
const tabIndex = line.indexOf('\t');
|
|
1519
|
+
if (tabIndex < 0) continue;
|
|
1520
|
+
const sha = line.slice(0, tabIndex);
|
|
1521
|
+
const ref = line.slice(tabIndex + 1);
|
|
1522
|
+
if (EXTERNAL_MARKETPLACE_SHA_RE.test(sha)) {
|
|
1523
|
+
hasRefs = true;
|
|
1524
|
+
if (ref && ref !== 'HEAD') refs.push(ref);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
const target = `refs/tags/${tag}`;
|
|
1528
|
+
return {
|
|
1529
|
+
hasRefs,
|
|
1530
|
+
tagExists: refs.includes(target) || refs.includes(`${target}^{}`),
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
async function defaultObserveFirstReleasePluginRepository(repo, tag, { githubHost = 'github.com' } = {}) {
|
|
1535
|
+
const url = `https://${githubHost}/${repo}.git`;
|
|
1536
|
+
let repositoryRefs;
|
|
1537
|
+
try {
|
|
1538
|
+
const { stdout } = await execFile(
|
|
1539
|
+
'git', ['ls-remote', '--symref', url],
|
|
1540
|
+
{ shell: false, encoding: 'utf8', timeout: 30000 },
|
|
1541
|
+
);
|
|
1542
|
+
repositoryRefs = parseFirstReleasePluginRepositoryRefs(String(stdout ?? ''), tag);
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
return { status: 'unknown', error: error.message };
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
let releaseExists = false;
|
|
1548
|
+
try {
|
|
1549
|
+
await execFile(
|
|
1550
|
+
'gh', ['api', `repos/${repo}/releases/tags/${tag}`],
|
|
1551
|
+
{
|
|
1552
|
+
shell: false,
|
|
1553
|
+
encoding: 'utf8',
|
|
1554
|
+
timeout: 30000,
|
|
1555
|
+
env: { ...process.env, GH_HOST: githubHost },
|
|
1556
|
+
},
|
|
1557
|
+
);
|
|
1558
|
+
releaseExists = true;
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
const diagnostic = `${error?.stderr ?? ''} ${error?.message ?? ''}`;
|
|
1561
|
+
if (!/\b404\b|not found/i.test(diagnostic)) {
|
|
1562
|
+
return { status: 'unknown', error: error.message };
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
return { status: repositoryRefs.hasRefs ? 'non-empty' : 'empty', tagExists: repositoryRefs.tagExists, releaseExists };
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function isFirstReleaseBootstrap(dist) {
|
|
1569
|
+
return dist?.firstReleaseBootstrap === FIRST_RELEASE_BOOTSTRAP_MODE;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1501
1572
|
/**
|
|
1502
1573
|
* Freeze the external marketplace HEAD for every claude/codex distribution
|
|
1503
1574
|
* that declares `marketplaceRepo` (production + online only). For each such
|
|
@@ -1518,6 +1589,8 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1518
1589
|
evidence,
|
|
1519
1590
|
observeHeadFn,
|
|
1520
1591
|
fetchIndexFn,
|
|
1592
|
+
productionAssets = null,
|
|
1593
|
+
observePluginRepositoryFn = defaultObserveFirstReleasePluginRepository,
|
|
1521
1594
|
}) {
|
|
1522
1595
|
const freezes = new Map();
|
|
1523
1596
|
for (let index = 0; index < unitResults.length; index += 1) {
|
|
@@ -1525,10 +1598,26 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1525
1598
|
const version = resolvedVersions[index];
|
|
1526
1599
|
const githubHost = unit.production?.githubHost ?? 'github.com';
|
|
1527
1600
|
for (const dist of unit.distributions ?? []) {
|
|
1601
|
+
if (isFirstReleaseBootstrap(dist) && dist.marketplaceSourceType !== 'standalone-index') {
|
|
1602
|
+
throw new ReleaseError(
|
|
1603
|
+
GATE_FAILED,
|
|
1604
|
+
`unit "${unit.id}" ${dist.type} first-release bootstrap requires marketplaceSourceType standalone-index`,
|
|
1605
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1528
1608
|
// 只处理 standalone-index 来源;bundled-family 不需要外部冻结。
|
|
1529
1609
|
if (dist.marketplaceSourceType !== 'standalone-index') continue;
|
|
1530
1610
|
// standalone-index 必须有 marketplaceRepo;没有则跳过(兼容旧配置)。
|
|
1531
|
-
if (!dist.marketplaceRepo)
|
|
1611
|
+
if (!dist.marketplaceRepo) {
|
|
1612
|
+
if (isFirstReleaseBootstrap(dist)) {
|
|
1613
|
+
throw new ReleaseError(
|
|
1614
|
+
GATE_FAILED,
|
|
1615
|
+
`unit "${unit.id}" ${dist.type} first-release bootstrap requires marketplaceRepo`,
|
|
1616
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1532
1621
|
const platform = PLATFORMS.find((p) => p.distributionType === dist.type);
|
|
1533
1622
|
if (!platform) {
|
|
1534
1623
|
throw new ReleaseError(
|
|
@@ -1545,6 +1634,13 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1545
1634
|
{ unitId: unit.id, marketplaceSourceType: dist.marketplaceSourceType },
|
|
1546
1635
|
);
|
|
1547
1636
|
}
|
|
1637
|
+
if (isFirstReleaseBootstrap(dist) && dist.type !== 'claude-plugin' && dist.type !== 'codex-plugin') {
|
|
1638
|
+
throw new ReleaseError(
|
|
1639
|
+
GATE_FAILED,
|
|
1640
|
+
`unit "${unit.id}" ${dist.type} first-release bootstrap is supported only for claude-plugin or codex-plugin standalone-index distributions`,
|
|
1641
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1548
1644
|
const observed = await observeHeadFn(dist.marketplaceRepo, { githubHost });
|
|
1549
1645
|
if (observed.status !== 'observed' || !EXTERNAL_MARKETPLACE_SHA_RE.test(observed.sha ?? '') || !observed.defaultBranch) {
|
|
1550
1646
|
throw new ReleaseError(
|
|
@@ -1581,23 +1677,65 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1581
1677
|
{ unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
|
|
1582
1678
|
);
|
|
1583
1679
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1680
|
+
if (!Array.isArray(marketplaceIndex.plugins)) {
|
|
1681
|
+
throw new ReleaseError(
|
|
1682
|
+
GATE_FAILED,
|
|
1683
|
+
`unit "${unit.id}" ${dist.type} external marketplace index plugins must be an array`,
|
|
1684
|
+
{ unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
const pluginEntries = marketplaceIndex.plugins.filter((entry) => entry && entry.name === dist.plugin);
|
|
1688
|
+
if (isFirstReleaseBootstrap(dist) && pluginEntries.length > 0) {
|
|
1689
|
+
throw new ReleaseError(
|
|
1690
|
+
GATE_FAILED,
|
|
1691
|
+
`unit "${unit.id}" first-release bootstrap requires the marketplace entry to be absent, found ${pluginEntries.length} matching entries`,
|
|
1692
|
+
{ unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
if (pluginEntries.length !== 1 && !isFirstReleaseBootstrap(dist)) {
|
|
1588
1696
|
throw new ReleaseError(
|
|
1589
1697
|
GATE_FAILED,
|
|
1590
1698
|
`unit "${unit.id}" external marketplace index must contain exactly one plugin entry named "${dist.plugin}", found ${pluginEntries.length}`,
|
|
1591
1699
|
{ unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
|
|
1592
1700
|
);
|
|
1593
1701
|
}
|
|
1594
|
-
|
|
1702
|
+
let selectedEntry = pluginEntries[0];
|
|
1703
|
+
if (!isFirstReleaseBootstrap(dist) && platform.marketplaceEntryCarriesVersion && selectedEntry.version !== version) {
|
|
1595
1704
|
throw new ReleaseError(
|
|
1596
1705
|
GATE_FAILED,
|
|
1597
1706
|
`unit "${unit.id}" external marketplace index entry version "${pluginEntries[0].version}" does not match target version "${version}"`,
|
|
1598
1707
|
{ unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
|
|
1599
1708
|
);
|
|
1600
1709
|
}
|
|
1710
|
+
if (isFirstReleaseBootstrap(dist)) {
|
|
1711
|
+
const asset = productionAssets?.[index];
|
|
1712
|
+
if (!asset || !EXTERNAL_MARKETPLACE_SHA_RE.test(asset.commit ?? '') || !asset.tag) {
|
|
1713
|
+
throw new ReleaseError(
|
|
1714
|
+
GATE_FAILED,
|
|
1715
|
+
`unit "${unit.id}" first-release bootstrap requires a frozen plugin tag and commit`,
|
|
1716
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
const pluginState = await observePluginRepositoryFn(unit.publicRepo, asset.tag, { githubHost });
|
|
1720
|
+
if (
|
|
1721
|
+
pluginState?.status !== 'empty'
|
|
1722
|
+
|| pluginState.tagExists !== false
|
|
1723
|
+
|| pluginState.releaseExists !== false
|
|
1724
|
+
) {
|
|
1725
|
+
throw new ReleaseError(
|
|
1726
|
+
GATE_FAILED,
|
|
1727
|
+
`unit "${unit.id}" first-release bootstrap requires an empty plugin repository without the target tag or release`,
|
|
1728
|
+
{ unitId: unit.id, publicRepo: unit.publicRepo, tag: asset.tag, observed: pluginState },
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
selectedEntry = buildExpectedStandaloneIndexInstallIdentity(platform, {
|
|
1732
|
+
name: dist.plugin,
|
|
1733
|
+
repo: unit.publicRepo,
|
|
1734
|
+
tag: asset.tag,
|
|
1735
|
+
sha: asset.commit,
|
|
1736
|
+
version,
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1601
1739
|
// marketplaceRef:Claude 使用默认分支名(name-ref),其余平台使用提交 SHA。
|
|
1602
1740
|
// 无 CLI 的平台(kimi、codebuddy)也必须能冻结,ref 用 SHA。
|
|
1603
1741
|
const marketplaceRef = platform.marketplaceRefForm === 'name' ? observed.defaultBranch : sha;
|
|
@@ -1612,7 +1750,10 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1612
1750
|
marketplaceRef,
|
|
1613
1751
|
marketplaceIndexPath: manifestPath,
|
|
1614
1752
|
marketplaceName: marketplaceIndex.name,
|
|
1615
|
-
selectedEntry
|
|
1753
|
+
selectedEntry,
|
|
1754
|
+
...(isFirstReleaseBootstrap(dist)
|
|
1755
|
+
? { firstReleaseBootstrap: FIRST_RELEASE_BOOTSTRAP_MODE, marketplaceIndexSha: null }
|
|
1756
|
+
: {}),
|
|
1616
1757
|
});
|
|
1617
1758
|
await evidence.append({
|
|
1618
1759
|
phase: 'external-marketplace-freeze',
|
|
@@ -1624,7 +1765,10 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1624
1765
|
marketplaceRef,
|
|
1625
1766
|
marketplaceIndexPath: manifestPath,
|
|
1626
1767
|
marketplaceName: marketplaceIndex.name,
|
|
1627
|
-
selectedEntry
|
|
1768
|
+
selectedEntry,
|
|
1769
|
+
...(isFirstReleaseBootstrap(dist)
|
|
1770
|
+
? { firstReleaseBootstrap: FIRST_RELEASE_BOOTSTRAP_MODE, marketplaceIndexSha: null }
|
|
1771
|
+
: {}),
|
|
1628
1772
|
defaultBranch: observed.defaultBranch,
|
|
1629
1773
|
});
|
|
1630
1774
|
}
|
|
@@ -2043,6 +2187,10 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
2043
2187
|
marketplaceIndexPath: freeze.marketplaceIndexPath,
|
|
2044
2188
|
marketplaceName: freeze.marketplaceName,
|
|
2045
2189
|
selectedEntry: freeze.selectedEntry,
|
|
2190
|
+
...(freeze.firstReleaseBootstrap ? {
|
|
2191
|
+
firstReleaseBootstrap: freeze.firstReleaseBootstrap,
|
|
2192
|
+
marketplaceIndexSha: null,
|
|
2193
|
+
} : {}),
|
|
2046
2194
|
} : {}),
|
|
2047
2195
|
},
|
|
2048
2196
|
expected: {
|
|
@@ -2058,6 +2206,10 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
2058
2206
|
entrySkillFound: true,
|
|
2059
2207
|
manifestDigest: asset.manifestDigest,
|
|
2060
2208
|
...(externalMarketplace ? { marketplaceLocation: 'external', marketplaceCommitSha: freeze.marketplaceCommitSha } : {}),
|
|
2209
|
+
...(externalMarketplace && freeze?.firstReleaseBootstrap ? {
|
|
2210
|
+
firstReleaseBootstrap: freeze.firstReleaseBootstrap,
|
|
2211
|
+
marketplaceIndexSha: null,
|
|
2212
|
+
} : {}),
|
|
2061
2213
|
},
|
|
2062
2214
|
status: 'PENDING',
|
|
2063
2215
|
});
|
|
@@ -4242,6 +4394,17 @@ export async function prepareRelease(options) {
|
|
|
4242
4394
|
// and validate the marketplace index entry at that sha before freezing the
|
|
4243
4395
|
// add-ref. The remote is only ever read (git ls-remote / gh api), never
|
|
4244
4396
|
// written, and it is only ever reached AFTER the local closure passed.
|
|
4397
|
+
for (const { unit } of unitResults) {
|
|
4398
|
+
for (const dist of unit.distributions ?? []) {
|
|
4399
|
+
if (isFirstReleaseBootstrap(dist) && (!production || offline)) {
|
|
4400
|
+
throw new ReleaseError(
|
|
4401
|
+
GATE_FAILED,
|
|
4402
|
+
`unit "${unit.id}" ${dist.type} first-release bootstrap requires online production prepare`,
|
|
4403
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
4404
|
+
);
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4245
4408
|
const externalMarketplaceFreezes = production
|
|
4246
4409
|
? await resolveExternalMarketplaceFreezes({
|
|
4247
4410
|
unitResults,
|
|
@@ -4250,6 +4413,8 @@ export async function prepareRelease(options) {
|
|
|
4250
4413
|
evidence,
|
|
4251
4414
|
observeHeadFn: options.observeExternalMarketplaceHeadFn ?? defaultObserveExternalMarketplaceHead,
|
|
4252
4415
|
fetchIndexFn: options.fetchExternalMarketplaceIndexFn ?? defaultFetchExternalMarketplaceIndex,
|
|
4416
|
+
productionAssets,
|
|
4417
|
+
observePluginRepositoryFn: options.observeFirstReleasePluginRepositoryFn ?? defaultObserveFirstReleasePluginRepository,
|
|
4253
4418
|
})
|
|
4254
4419
|
: new Map();
|
|
4255
4420
|
|
|
@@ -4353,6 +4518,10 @@ export async function prepareRelease(options) {
|
|
|
4353
4518
|
frozenDist.marketplaceIndexPath = freeze.marketplaceIndexPath;
|
|
4354
4519
|
frozenDist.marketplaceName = freeze.marketplaceName;
|
|
4355
4520
|
frozenDist.selectedEntry = freeze.selectedEntry;
|
|
4521
|
+
if (freeze.firstReleaseBootstrap) {
|
|
4522
|
+
frozenDist.firstReleaseBootstrap = freeze.firstReleaseBootstrap;
|
|
4523
|
+
frozenDist.marketplaceIndexSha = null;
|
|
4524
|
+
}
|
|
4356
4525
|
}
|
|
4357
4526
|
}
|
|
4358
4527
|
|
package/src/commands/ship.mjs
CHANGED
|
@@ -174,7 +174,8 @@ async function defaultDependencies() {
|
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
function publicState(state) {
|
|
177
|
+
function publicState(state, { postRelease } = {}) {
|
|
178
|
+
const visiblePostRelease = postRelease === undefined ? state.postRelease : postRelease;
|
|
178
179
|
return {
|
|
179
180
|
command: 'ship',
|
|
180
181
|
status: state.status,
|
|
@@ -199,7 +200,7 @@ function publicState(state) {
|
|
|
199
200
|
...(state.requirements ? { requirements: state.requirements } : {}),
|
|
200
201
|
...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
|
|
201
202
|
...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
|
|
202
|
-
...(
|
|
203
|
+
...(visiblePostRelease ? { postRelease: visiblePostRelease } : {}),
|
|
203
204
|
externalActionsIncludedInPlanApproval: true,
|
|
204
205
|
verificationGatesIncludedInPlanApproval: true,
|
|
205
206
|
postPublishCheckpointApprovalsIncludedInPlanApproval: false,
|
|
@@ -718,15 +719,6 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
718
719
|
updatedAt: new Date().toISOString(),
|
|
719
720
|
};
|
|
720
721
|
await writeJsonAtomic(statePath, state);
|
|
721
|
-
if (verified.status === 'VERIFIED') {
|
|
722
|
-
try {
|
|
723
|
-
state.postRelease = derivePostReleaseChecklist(plan);
|
|
724
|
-
} catch (error) {
|
|
725
|
-
state.postRelease = unavailablePostReleaseChecklist(plan, error);
|
|
726
|
-
}
|
|
727
|
-
state.updatedAt = new Date().toISOString();
|
|
728
|
-
await writeJsonAtomic(statePath, state);
|
|
729
|
-
}
|
|
730
722
|
} catch (error) {
|
|
731
723
|
if (error?.code !== CONSUMER_VERIFICATION_DEFERRED) throw error;
|
|
732
724
|
state = {
|
|
@@ -835,5 +827,28 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
835
827
|
}
|
|
836
828
|
}
|
|
837
829
|
|
|
830
|
+
if (state.status === 'VERIFIED') {
|
|
831
|
+
const finalPlan = JSON.parse(await readFile(state.planPath, 'utf8'));
|
|
832
|
+
const hasPostVerify = normalizePostPublishView(finalPlan).some((declaration) => (
|
|
833
|
+
(declaration.hooks ?? []).some((hook) => hook.phase === 'postVerify')
|
|
834
|
+
));
|
|
835
|
+
const localFinishRunPath = hasPostVerify
|
|
836
|
+
? (state.postVerify?.status === 'DISTRIBUTED' ? state.postVerify.runPath : undefined)
|
|
837
|
+
: state.verifyRunPath;
|
|
838
|
+
let postRelease;
|
|
839
|
+
try {
|
|
840
|
+
postRelease = derivePostReleaseChecklist(finalPlan, {
|
|
841
|
+
root,
|
|
842
|
+
statePath,
|
|
843
|
+
unitIds: state.selectedUnitIds,
|
|
844
|
+
runPath: localFinishRunPath,
|
|
845
|
+
postVerifyComplete: !hasPostVerify || state.postVerify?.status === 'DISTRIBUTED',
|
|
846
|
+
});
|
|
847
|
+
} catch (error) {
|
|
848
|
+
postRelease = unavailablePostReleaseChecklist(finalPlan, error);
|
|
849
|
+
}
|
|
850
|
+
return publicState(state, { postRelease });
|
|
851
|
+
}
|
|
852
|
+
|
|
838
853
|
return publicState(state);
|
|
839
854
|
}
|