release-skill 0.9.3 → 0.9.5
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 +58 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +52 -16
- package/README.zh-CN.md +46 -16
- 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 +1513 -352
- 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 +11 -4
- package/adapters/claude/skills/release-prepare/SKILL.md +11 -3
- package/adapters/claude/skills/release-verify/SKILL.md +2 -1
- 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 +1513 -352
- 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 +11 -4
- package/adapters/codex/skills/release-prepare/SKILL.md +11 -3
- package/adapters/codex/skills/release-verify/SKILL.md +2 -1
- 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 +1513 -352
- 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 +11 -4
- package/adapters/kimi/skills/release-prepare/SKILL.md +11 -3
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -1
- 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 +1513 -352
- 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 +11 -4
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +11 -3
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -1
- package/bin/consumer-contract-vectors.json +7 -7
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/release-skill-cli.mjs +113 -22
- package/bin/release-skill.bundle.mjs +1513 -352
- 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/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 +11 -4
- package/skills/release-prepare/SKILL.md +11 -3
- package/skills/release-verify/SKILL.md +2 -1
- package/skills-src/release-finish/SKILL.md +5 -5
- package/skills-src/release-help/SKILL.md +11 -4
- package/skills-src/release-prepare/SKILL.md +11 -3
- package/skills-src/release-verify/SKILL.md +2 -1
- package/src/commands/post-release-local.mjs +373 -26
- package/src/commands/prepare.mjs +361 -16
- package/src/commands/ship.mjs +190 -13
- package/src/commands/verify.mjs +175 -2
- package/src/core/foundation-plugin-verification.mjs +22 -2
- package/src/core/plan.mjs +45 -1
- package/src/platforms/registry.mjs +97 -0
- package/src/producers/foundation-resource-projection.mjs +3 -3
- package/src/readme/contract.mjs +23 -3
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
|
});
|
|
@@ -2917,6 +3069,142 @@ async function runPreHookPublicSurfaceGate({
|
|
|
2917
3069
|
}
|
|
2918
3070
|
}
|
|
2919
3071
|
|
|
3072
|
+
/**
|
|
3073
|
+
* Resolve the operator's pre-freeze release-unit request against the complete
|
|
3074
|
+
* validated project configuration. The returned units always follow config
|
|
3075
|
+
* declaration order so plan ordering and digests never depend on argv order.
|
|
3076
|
+
*/
|
|
3077
|
+
function prepareRetryPathArg(path) {
|
|
3078
|
+
return relative(process.cwd(), resolve(path)) || '.';
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
function buildPrepareRetryArgv(options, unitIds) {
|
|
3082
|
+
const argv = [
|
|
3083
|
+
'release-skill',
|
|
3084
|
+
'prepare',
|
|
3085
|
+
'--root',
|
|
3086
|
+
prepareRetryPathArg(options.root),
|
|
3087
|
+
];
|
|
3088
|
+
if (typeof options.version === 'string' && options.version.length > 0) {
|
|
3089
|
+
argv.push('--target-version', options.version);
|
|
3090
|
+
}
|
|
3091
|
+
argv.push(options.offline ? '--offline' : '--online');
|
|
3092
|
+
if (options.production) argv.push('--production');
|
|
3093
|
+
argv.push('--workflow', options.workflow);
|
|
3094
|
+
if (typeof options.testSelection === 'string') {
|
|
3095
|
+
argv.push('--test-selection', options.testSelection);
|
|
3096
|
+
}
|
|
3097
|
+
if (typeof options.output === 'string') {
|
|
3098
|
+
argv.push('--output', prepareRetryPathArg(options.output));
|
|
3099
|
+
}
|
|
3100
|
+
// A production --run-dir is single-use by contract and this failed attempt
|
|
3101
|
+
// has already created it. Let the retry allocate a fresh evidence directory.
|
|
3102
|
+
if (options.hookCache === false) argv.push('--no-hook-cache');
|
|
3103
|
+
for (const unitId of unitIds) argv.push('--unit', unitId);
|
|
3104
|
+
return argv;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
/**
|
|
3108
|
+
* Keep the deferred-unit evidence and operator guidance on one exact argv.
|
|
3109
|
+
* The command points back to the caller's project root and carries the same
|
|
3110
|
+
* prepare mode while selecting only the units that were deferred.
|
|
3111
|
+
*/
|
|
3112
|
+
function buildDeferredReleaseDiagnostics(options, deferredUnitIds) {
|
|
3113
|
+
if (deferredUnitIds.length === 0) return null;
|
|
3114
|
+
return {
|
|
3115
|
+
deferredUnitIds: [...deferredUnitIds],
|
|
3116
|
+
nextPrepareArgv: buildPrepareRetryArgv(options, deferredUnitIds),
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
function resolveRequestedReleaseUnits(config, requestedUnitIds, retryOptions) {
|
|
3121
|
+
const availableUnits = config.releaseUnits ?? [];
|
|
3122
|
+
const availableUnitIds = availableUnits.map((unit) => unit.id);
|
|
3123
|
+
if (requestedUnitIds === undefined) {
|
|
3124
|
+
return {
|
|
3125
|
+
explicit: false,
|
|
3126
|
+
selectedUnits: availableUnits,
|
|
3127
|
+
selectedUnitIds: availableUnitIds,
|
|
3128
|
+
deferredUnitIds: [],
|
|
3129
|
+
receiptSelected: Boolean(config.publicSourceAuthorityReceipt),
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
if (!Array.isArray(requestedUnitIds) || requestedUnitIds.length === 0) {
|
|
3133
|
+
throw new ReleaseError(
|
|
3134
|
+
CONFIG_INVALID,
|
|
3135
|
+
'explicit release-unit selection must contain at least one unit id',
|
|
3136
|
+
{ requestedUnitIds, availableUnitIds },
|
|
3137
|
+
);
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
const invalidUnitIds = requestedUnitIds.filter(
|
|
3141
|
+
(unitId) => typeof unitId !== 'string' || unitId.length === 0,
|
|
3142
|
+
);
|
|
3143
|
+
const seen = new Set();
|
|
3144
|
+
const duplicateUnitIds = [];
|
|
3145
|
+
for (const unitId of requestedUnitIds) {
|
|
3146
|
+
if (seen.has(unitId) && !duplicateUnitIds.includes(unitId)) duplicateUnitIds.push(unitId);
|
|
3147
|
+
seen.add(unitId);
|
|
3148
|
+
}
|
|
3149
|
+
const availableSet = new Set(availableUnitIds);
|
|
3150
|
+
const unknownUnitIds = requestedUnitIds.filter(
|
|
3151
|
+
(unitId) => typeof unitId === 'string' && unitId.length > 0 && !availableSet.has(unitId),
|
|
3152
|
+
);
|
|
3153
|
+
if (invalidUnitIds.length > 0 || duplicateUnitIds.length > 0 || unknownUnitIds.length > 0) {
|
|
3154
|
+
throw new ReleaseError(
|
|
3155
|
+
CONFIG_INVALID,
|
|
3156
|
+
'release-unit selection contains empty, duplicate, or unknown unit ids',
|
|
3157
|
+
{
|
|
3158
|
+
requestedUnitIds,
|
|
3159
|
+
availableUnitIds,
|
|
3160
|
+
invalidUnitIds,
|
|
3161
|
+
duplicateUnitIds,
|
|
3162
|
+
unknownUnitIds,
|
|
3163
|
+
},
|
|
3164
|
+
);
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
const selectedSet = new Set(requestedUnitIds);
|
|
3168
|
+
const selectedUnits = availableUnits.filter((unit) => selectedSet.has(unit.id));
|
|
3169
|
+
const selectedUnitIds = selectedUnits.map((unit) => unit.id);
|
|
3170
|
+
const deferredUnitIds = availableUnitIds.filter((unitId) => !selectedSet.has(unitId));
|
|
3171
|
+
const deferredReleaseDiagnostics = buildDeferredReleaseDiagnostics(retryOptions, deferredUnitIds);
|
|
3172
|
+
const receipt = config.publicSourceAuthorityReceipt;
|
|
3173
|
+
let receiptSelected = false;
|
|
3174
|
+
if (receipt) {
|
|
3175
|
+
const receiptSet = new Set([receipt.coordinatorUnitId, ...receipt.subjectUnitIds]);
|
|
3176
|
+
const dependencyUnitIds = availableUnitIds.filter((unitId) => receiptSet.has(unitId));
|
|
3177
|
+
receiptSelected = dependencyUnitIds.some((unitId) => selectedSet.has(unitId));
|
|
3178
|
+
if (receiptSelected) {
|
|
3179
|
+
const missingUnitIds = dependencyUnitIds.filter((unitId) => !selectedSet.has(unitId));
|
|
3180
|
+
if (missingUnitIds.length > 0) {
|
|
3181
|
+
const retryUnitIds = availableUnitIds.filter(
|
|
3182
|
+
(unitId) => selectedSet.has(unitId) || receiptSet.has(unitId),
|
|
3183
|
+
);
|
|
3184
|
+
throw new ReleaseError(
|
|
3185
|
+
GATE_FAILED,
|
|
3186
|
+
'release-unit selection does not include the complete public source-authority dependency closure',
|
|
3187
|
+
{
|
|
3188
|
+
selectedUnitIds,
|
|
3189
|
+
missingUnitIds,
|
|
3190
|
+
dependencyUnitIds,
|
|
3191
|
+
retryArgv: buildPrepareRetryArgv(retryOptions, retryUnitIds),
|
|
3192
|
+
},
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
return {
|
|
3199
|
+
explicit: true,
|
|
3200
|
+
selectedUnits,
|
|
3201
|
+
selectedUnitIds,
|
|
3202
|
+
deferredUnitIds,
|
|
3203
|
+
deferredReleaseDiagnostics,
|
|
3204
|
+
receiptSelected,
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
|
|
2920
3208
|
export async function prepareRelease(options) {
|
|
2921
3209
|
const {
|
|
2922
3210
|
root,
|
|
@@ -2931,6 +3219,7 @@ export async function prepareRelease(options) {
|
|
|
2931
3219
|
workflow = 'full',
|
|
2932
3220
|
observePreviousPublicBaselineFn,
|
|
2933
3221
|
testSelection = 'full',
|
|
3222
|
+
unitIds,
|
|
2934
3223
|
} = options ?? {};
|
|
2935
3224
|
|
|
2936
3225
|
// --- Workflow profile (H5) ---
|
|
@@ -3039,7 +3328,20 @@ export async function prepareRelease(options) {
|
|
|
3039
3328
|
await evidence.append({ phase: 'config', status: 'started' });
|
|
3040
3329
|
|
|
3041
3330
|
const { config, configPath, configDigest } = await loadProjectConfig({ root: realRoot });
|
|
3042
|
-
const
|
|
3331
|
+
const releaseSelection = resolveRequestedReleaseUnits(config, unitIds, {
|
|
3332
|
+
root,
|
|
3333
|
+
version,
|
|
3334
|
+
offline,
|
|
3335
|
+
production,
|
|
3336
|
+
workflow,
|
|
3337
|
+
testSelection: typeof options?.testSelection === 'string' ? testSelection : undefined,
|
|
3338
|
+
output,
|
|
3339
|
+
hookCache: options?.hookCache,
|
|
3340
|
+
});
|
|
3341
|
+
const configUnits = releaseSelection.selectedUnits;
|
|
3342
|
+
const selectedConfig = { ...config, releaseUnits: configUnits };
|
|
3343
|
+
const selectedUnitIdSet = new Set(releaseSelection.selectedUnitIds);
|
|
3344
|
+
const adoptionWarnings = collectExpectedPublicSurfaceAdoptionWarnings(selectedConfig);
|
|
3043
3345
|
// Mutable operator-facing warning list: seeded from the adoption warnings,
|
|
3044
3346
|
// appended by later gates (O5 origin-ahead). Returned as `warnings`.
|
|
3045
3347
|
const runWarnings = [...adoptionWarnings];
|
|
@@ -3057,6 +3359,18 @@ export async function prepareRelease(options) {
|
|
|
3057
3359
|
...warning,
|
|
3058
3360
|
});
|
|
3059
3361
|
}
|
|
3362
|
+
if (releaseSelection.explicit) {
|
|
3363
|
+
await evidence.append({
|
|
3364
|
+
phase: 'release-scope',
|
|
3365
|
+
status: 'selected',
|
|
3366
|
+
mode: 'selected',
|
|
3367
|
+
selectedUnitIds: releaseSelection.selectedUnitIds,
|
|
3368
|
+
deferredUnitIds: releaseSelection.deferredUnitIds,
|
|
3369
|
+
...(releaseSelection.deferredReleaseDiagnostics
|
|
3370
|
+
? { nextPrepareArgv: releaseSelection.deferredReleaseDiagnostics.nextPrepareArgv }
|
|
3371
|
+
: {}),
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3060
3374
|
|
|
3061
3375
|
// --- Step 1-fresh: Bundle freshness gate (BUNDLE_STALE, fail-closed) ---
|
|
3062
3376
|
// 2026-08-18 investigation §4.2: a stale bin/release-skill.bundle.mjs
|
|
@@ -3132,7 +3446,6 @@ export async function prepareRelease(options) {
|
|
|
3132
3446
|
// gate, baseline, snapshot, remote check, or plan write runs. Units
|
|
3133
3447
|
// without releaseDocuments keep the exact legacy behaviour (the gate
|
|
3134
3448
|
// appends no evidence and performs no check).
|
|
3135
|
-
const configUnits = config.releaseUnits ?? [];
|
|
3136
3449
|
const resolvedVersions = await resolveAllUnitVersions(
|
|
3137
3450
|
configUnits,
|
|
3138
3451
|
realRoot,
|
|
@@ -3330,7 +3643,8 @@ export async function prepareRelease(options) {
|
|
|
3330
3643
|
});
|
|
3331
3644
|
}
|
|
3332
3645
|
|
|
3333
|
-
const declaredVerificationGates = config.verificationGates ?? []
|
|
3646
|
+
const declaredVerificationGates = (config.verificationGates ?? [])
|
|
3647
|
+
.filter((gate) => selectedUnitIdSet.has(gate.scope.unit));
|
|
3334
3648
|
if (declaredVerificationGates.length > 0) {
|
|
3335
3649
|
await evidence.append({
|
|
3336
3650
|
phase: 'verification-gate-authorization',
|
|
@@ -3435,7 +3749,7 @@ export async function prepareRelease(options) {
|
|
|
3435
3749
|
// R93-01: optional ephemeral public-surface check immediately before the
|
|
3436
3750
|
// first hook. Its staging and closure receipt never enter the plan.
|
|
3437
3751
|
await runPreHookPublicSurfaceGate({
|
|
3438
|
-
config,
|
|
3752
|
+
config: selectedConfig,
|
|
3439
3753
|
root: realRoot,
|
|
3440
3754
|
resolvedVersions,
|
|
3441
3755
|
evidence,
|
|
@@ -3894,7 +4208,7 @@ export async function prepareRelease(options) {
|
|
|
3894
4208
|
|
|
3895
4209
|
// --- Step 5: Build snapshots, scan, and evaluate README ---
|
|
3896
4210
|
const { unitResults, snapshotDigests } = await processSnapshots(
|
|
3897
|
-
|
|
4211
|
+
selectedConfig, realRoot, evidence, runDir, production,
|
|
3898
4212
|
);
|
|
3899
4213
|
|
|
3900
4214
|
// Snapshot gates always run on disposable writable copies. The public
|
|
@@ -4080,6 +4394,17 @@ export async function prepareRelease(options) {
|
|
|
4080
4394
|
// and validate the marketplace index entry at that sha before freezing the
|
|
4081
4395
|
// add-ref. The remote is only ever read (git ls-remote / gh api), never
|
|
4082
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
|
+
}
|
|
4083
4408
|
const externalMarketplaceFreezes = production
|
|
4084
4409
|
? await resolveExternalMarketplaceFreezes({
|
|
4085
4410
|
unitResults,
|
|
@@ -4088,6 +4413,8 @@ export async function prepareRelease(options) {
|
|
|
4088
4413
|
evidence,
|
|
4089
4414
|
observeHeadFn: options.observeExternalMarketplaceHeadFn ?? defaultObserveExternalMarketplaceHead,
|
|
4090
4415
|
fetchIndexFn: options.fetchExternalMarketplaceIndexFn ?? defaultFetchExternalMarketplaceIndex,
|
|
4416
|
+
productionAssets,
|
|
4417
|
+
observePluginRepositoryFn: options.observeFirstReleasePluginRepositoryFn ?? defaultObserveFirstReleasePluginRepository,
|
|
4091
4418
|
})
|
|
4092
4419
|
: new Map();
|
|
4093
4420
|
|
|
@@ -4191,6 +4518,10 @@ export async function prepareRelease(options) {
|
|
|
4191
4518
|
frozenDist.marketplaceIndexPath = freeze.marketplaceIndexPath;
|
|
4192
4519
|
frozenDist.marketplaceName = freeze.marketplaceName;
|
|
4193
4520
|
frozenDist.selectedEntry = freeze.selectedEntry;
|
|
4521
|
+
if (freeze.firstReleaseBootstrap) {
|
|
4522
|
+
frozenDist.firstReleaseBootstrap = freeze.firstReleaseBootstrap;
|
|
4523
|
+
frozenDist.marketplaceIndexSha = null;
|
|
4524
|
+
}
|
|
4194
4525
|
}
|
|
4195
4526
|
}
|
|
4196
4527
|
|
|
@@ -4280,7 +4611,7 @@ export async function prepareRelease(options) {
|
|
|
4280
4611
|
}
|
|
4281
4612
|
|
|
4282
4613
|
let publicSourceAuthorityReceipt = null;
|
|
4283
|
-
if (config.publicSourceAuthorityReceipt) {
|
|
4614
|
+
if (config.publicSourceAuthorityReceipt && releaseSelection.receiptSelected) {
|
|
4284
4615
|
// P3 (ruling 7): config scenario A (no-publish-needed) means the public
|
|
4285
4616
|
// bytes are unchanged and nothing will be published — a configured
|
|
4286
4617
|
// public source receipt must not require or generate a nonexistent
|
|
@@ -4524,7 +4855,7 @@ export async function prepareRelease(options) {
|
|
|
4524
4855
|
},
|
|
4525
4856
|
}
|
|
4526
4857
|
: {}),
|
|
4527
|
-
verificationGates:
|
|
4858
|
+
verificationGates: declaredVerificationGates,
|
|
4528
4859
|
snapshotDigest: overallSnapshotDigest,
|
|
4529
4860
|
...(humanConsumersStrategy ? { humanConsumersStrategy } : {}),
|
|
4530
4861
|
...(production ? {
|
|
@@ -4608,6 +4939,13 @@ export async function prepareRelease(options) {
|
|
|
4608
4939
|
// carry no such warning. The prepare command's own defaults are unchanged —
|
|
4609
4940
|
// this only enriches the result, never flips offline/production.
|
|
4610
4941
|
const nextSteps = [];
|
|
4942
|
+
if (releaseSelection.explicit && releaseSelection.deferredUnitIds.length > 0) {
|
|
4943
|
+
nextSteps.push({
|
|
4944
|
+
code: 'DEFERRED_RELEASE_UNITS',
|
|
4945
|
+
message: `Prepare the deferred release units separately: ${releaseSelection.deferredUnitIds.join(', ')}`,
|
|
4946
|
+
argv: releaseSelection.deferredReleaseDiagnostics.nextPrepareArgv,
|
|
4947
|
+
});
|
|
4948
|
+
}
|
|
4611
4949
|
if (!production) {
|
|
4612
4950
|
nextSteps.push({
|
|
4613
4951
|
code: 'NON_PRODUCTION_PLAN_NOT_PUBLISHABLE',
|
|
@@ -4624,6 +4962,13 @@ export async function prepareRelease(options) {
|
|
|
4624
4962
|
evidenceDir,
|
|
4625
4963
|
warnings: runWarnings,
|
|
4626
4964
|
nextSteps,
|
|
4965
|
+
...(releaseSelection.explicit ? {
|
|
4966
|
+
releaseScope: {
|
|
4967
|
+
mode: 'selected',
|
|
4968
|
+
selectedUnitIds: releaseSelection.selectedUnitIds,
|
|
4969
|
+
deferredUnitIds: releaseSelection.deferredUnitIds,
|
|
4970
|
+
},
|
|
4971
|
+
} : {}),
|
|
4627
4972
|
};
|
|
4628
4973
|
} catch (err) {
|
|
4629
4974
|
// Record failure evidence (best effort). A broken clock can make the
|