@wipcomputer/wip-release 1.9.73 → 1.9.75
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/cli.js +4 -0
- package/core.mjs +383 -23
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ const skipWorktreeCheck = args.includes('--skip-worktree-check');
|
|
|
24
24
|
const skipTechDocsCheck = args.includes('--skip-tech-docs-check');
|
|
25
25
|
const skipCoverageCheck = args.includes('--skip-coverage-check');
|
|
26
26
|
const allowSubToolDrift = args.includes('--allow-sub-tool-drift');
|
|
27
|
+
const noDeployPublic = args.includes('--no-deploy-public');
|
|
27
28
|
const wantReleaseNotes = args.includes('--release-notes');
|
|
28
29
|
const noReleaseNotes = args.includes('--no-release-notes');
|
|
29
30
|
const notesFilePath = flag('notes-file');
|
|
@@ -174,6 +175,7 @@ Flags:
|
|
|
174
175
|
--skip-stale-check Skip stale remote branch check
|
|
175
176
|
--skip-worktree-check Skip main-branch + worktree guard (break-glass only)
|
|
176
177
|
--allow-sub-tool-drift Allow release even if a sub-tool's files changed since the last tag without a version bump (error by default)
|
|
178
|
+
--no-deploy-public Skip the deploy-public.sh step at the end of stable and prerelease flows (runs by default for -private repos)
|
|
177
179
|
|
|
178
180
|
Release notes (REQUIRED for stable, optional for other tracks):
|
|
179
181
|
1. --notes-file=path Explicit file path
|
|
@@ -224,6 +226,7 @@ if (level === 'alpha' || level === 'beta') {
|
|
|
224
226
|
publishReleaseNotes: level === 'alpha' ? wantReleaseNotes : !noReleaseNotes,
|
|
225
227
|
skipWorktreeCheck,
|
|
226
228
|
allowSubToolDrift,
|
|
229
|
+
noDeployPublic,
|
|
227
230
|
}).catch(err => {
|
|
228
231
|
console.error(` \u2717 ${err.message}`);
|
|
229
232
|
process.exit(1);
|
|
@@ -258,6 +261,7 @@ if (level === 'alpha' || level === 'beta') {
|
|
|
258
261
|
skipTechDocsCheck,
|
|
259
262
|
skipCoverageCheck,
|
|
260
263
|
allowSubToolDrift,
|
|
264
|
+
noDeployPublic,
|
|
261
265
|
}).catch(err => {
|
|
262
266
|
console.error(` \u2717 ${err.message}`);
|
|
263
267
|
process.exit(1);
|
package/core.mjs
CHANGED
|
@@ -1408,6 +1408,67 @@ function enforceMainBranchGuard(repoPath, skipWorktreeCheck) {
|
|
|
1408
1408
|
* Related: `ai/product/bugs/release-pipeline/2026-04-05--cc-mini--release-pipeline-master-plan.md`
|
|
1409
1409
|
* Phase 8.
|
|
1410
1410
|
*/
|
|
1411
|
+
/**
|
|
1412
|
+
* Phase 5: Auto-bump sub-tool patch versions when their files changed since last tag.
|
|
1413
|
+
* Called before validateSubToolVersions so drift is fixed before validation runs.
|
|
1414
|
+
* Returns the number of sub-tools bumped.
|
|
1415
|
+
*/
|
|
1416
|
+
function autoFixSubToolVersions(repoPath) {
|
|
1417
|
+
const toolsDir = join(repoPath, 'tools');
|
|
1418
|
+
if (!existsSync(toolsDir)) return 0;
|
|
1419
|
+
let lastTag = null;
|
|
1420
|
+
try {
|
|
1421
|
+
lastTag = execFileSync('git', ['describe', '--tags', '--abbrev=0'], {
|
|
1422
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1423
|
+
}).trim();
|
|
1424
|
+
} catch {
|
|
1425
|
+
return 0;
|
|
1426
|
+
}
|
|
1427
|
+
if (!lastTag) return 0;
|
|
1428
|
+
|
|
1429
|
+
let bumped = 0;
|
|
1430
|
+
let entries;
|
|
1431
|
+
try {
|
|
1432
|
+
entries = readdirSync(toolsDir, { withFileTypes: true });
|
|
1433
|
+
} catch {
|
|
1434
|
+
return 0;
|
|
1435
|
+
}
|
|
1436
|
+
for (const entry of entries) {
|
|
1437
|
+
if (!entry.isDirectory()) continue;
|
|
1438
|
+
const subDir = `tools/${entry.name}`;
|
|
1439
|
+
const subPkgPath = join(toolsDir, entry.name, 'package.json');
|
|
1440
|
+
if (!existsSync(subPkgPath)) continue;
|
|
1441
|
+
try {
|
|
1442
|
+
const diff = execFileSync('git', ['diff', '--name-only', lastTag, 'HEAD', '--', subDir], {
|
|
1443
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1444
|
+
}).trim();
|
|
1445
|
+
if (!diff) continue;
|
|
1446
|
+
const subPkg = JSON.parse(readFileSync(subPkgPath, 'utf8'));
|
|
1447
|
+
const currentSubVersion = subPkg.version;
|
|
1448
|
+
let oldSubVersion = null;
|
|
1449
|
+
try {
|
|
1450
|
+
oldSubVersion = JSON.parse(
|
|
1451
|
+
execFileSync('git', ['show', `${lastTag}:${subDir}/package.json`], {
|
|
1452
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1453
|
+
})
|
|
1454
|
+
).version;
|
|
1455
|
+
} catch {}
|
|
1456
|
+
if (currentSubVersion === oldSubVersion) {
|
|
1457
|
+
const parts = currentSubVersion.split('.');
|
|
1458
|
+
parts[2] = String(Number(parts[2]) + 1);
|
|
1459
|
+
const newSubVersion = parts.join('.');
|
|
1460
|
+
subPkg.version = newSubVersion;
|
|
1461
|
+
writeFileSync(subPkgPath, JSON.stringify(subPkg, null, 2) + '\n');
|
|
1462
|
+
console.log(` ✓ Auto-bumped ${entry.name}: ${currentSubVersion} -> ${newSubVersion}`);
|
|
1463
|
+
bumped++;
|
|
1464
|
+
}
|
|
1465
|
+
} catch (err) {
|
|
1466
|
+
console.log(` ! Auto-bump failed for ${entry.name}: ${err.message}`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return bumped;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1411
1472
|
function validateSubToolVersions(repoPath, allowSubToolDrift) {
|
|
1412
1473
|
const toolsDir = join(repoPath, 'tools');
|
|
1413
1474
|
if (!existsSync(toolsDir)) {
|
|
@@ -1626,6 +1687,79 @@ function logPushFailure(result, tag) {
|
|
|
1626
1687
|
}
|
|
1627
1688
|
}
|
|
1628
1689
|
|
|
1690
|
+
/**
|
|
1691
|
+
* Resolve the public GitHub repo name for a private-to-public mirror setup.
|
|
1692
|
+
*
|
|
1693
|
+
* Strategy:
|
|
1694
|
+
* 1. Read `origin` remote URL (e.g. git@github.com:owner/repo-private.git)
|
|
1695
|
+
* 2. Strip `-private` suffix to get the public repo name
|
|
1696
|
+
* 3. Return `owner/repo` format suitable for gh CLI
|
|
1697
|
+
*
|
|
1698
|
+
* Returns null if the remote URL cannot be parsed or the repo name does not
|
|
1699
|
+
* end in `-private` (in which case the caller should skip deploy-public as a
|
|
1700
|
+
* no-op rather than error).
|
|
1701
|
+
*/
|
|
1702
|
+
function resolvePublicRepoName(repoPath) {
|
|
1703
|
+
try {
|
|
1704
|
+
const remoteUrl = execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
1705
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1706
|
+
}).trim();
|
|
1707
|
+
// Match owner/repo from either SSH (git@github.com:owner/repo.git) or HTTPS
|
|
1708
|
+
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/(.+?)(\.git)?$/);
|
|
1709
|
+
if (!match) return null;
|
|
1710
|
+
const [, owner, repoName] = match;
|
|
1711
|
+
if (!repoName.endsWith('-private')) {
|
|
1712
|
+
return null;
|
|
1713
|
+
}
|
|
1714
|
+
return `${owner}/${repoName.replace(/-private$/, '')}`;
|
|
1715
|
+
} catch {
|
|
1716
|
+
return null;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/**
|
|
1721
|
+
* Run deploy-public.sh as the final step of the release pipeline.
|
|
1722
|
+
*
|
|
1723
|
+
* Previously deploy-public was a separate manual step. Every release that
|
|
1724
|
+
* forgot it left the public mirror stale, so `ldm install` pulled old code
|
|
1725
|
+
* from the public repo. Now wip-release invokes it automatically for stable
|
|
1726
|
+
* and prerelease tracks (hotfix still skips, per prior convention).
|
|
1727
|
+
*
|
|
1728
|
+
* Callable with `--no-deploy-public` to opt out.
|
|
1729
|
+
*
|
|
1730
|
+
* Skips silently if:
|
|
1731
|
+
* - The repo has no tools/deploy-public/deploy-public.sh (not a toolbox-
|
|
1732
|
+
* style repo, or deploy-public is provided by a parent install)
|
|
1733
|
+
* - The origin remote is not a -private repo (no public mirror to sync)
|
|
1734
|
+
*
|
|
1735
|
+
* Related: `ai/product/bugs/release-pipeline/2026-04-05--cc-mini--release-pipeline-master-plan.md` Phase 6.
|
|
1736
|
+
*/
|
|
1737
|
+
function runDeployPublic(repoPath, { skip } = {}) {
|
|
1738
|
+
if (skip) {
|
|
1739
|
+
return { ok: true, skipped: true, reason: 'flag' };
|
|
1740
|
+
}
|
|
1741
|
+
const scriptPath = join(repoPath, 'tools', 'deploy-public', 'deploy-public.sh');
|
|
1742
|
+
if (!existsSync(scriptPath)) {
|
|
1743
|
+
return { ok: true, skipped: true, reason: 'no-script' };
|
|
1744
|
+
}
|
|
1745
|
+
const publicRepo = resolvePublicRepoName(repoPath);
|
|
1746
|
+
if (!publicRepo) {
|
|
1747
|
+
return { ok: true, skipped: true, reason: 'not-private-repo' };
|
|
1748
|
+
}
|
|
1749
|
+
try {
|
|
1750
|
+
execFileSync('bash', [scriptPath, repoPath, publicRepo], {
|
|
1751
|
+
cwd: repoPath, stdio: 'inherit',
|
|
1752
|
+
});
|
|
1753
|
+
return { ok: true, publicRepo };
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
return {
|
|
1756
|
+
ok: false,
|
|
1757
|
+
detail: String(err?.stderr ?? err?.message ?? err),
|
|
1758
|
+
publicRepo,
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1629
1763
|
function logMainBranchGuardFailure(result) {
|
|
1630
1764
|
if (result.reason === 'linked-worktree') {
|
|
1631
1765
|
console.log(` \u2717 wip-release must run from the main working tree, not a worktree.`);
|
|
@@ -1645,7 +1779,7 @@ function logMainBranchGuardFailure(result) {
|
|
|
1645
1779
|
/**
|
|
1646
1780
|
* Run the full release pipeline.
|
|
1647
1781
|
*/
|
|
1648
|
-
export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck, skipWorktreeCheck, skipTechDocsCheck, skipCoverageCheck, allowSubToolDrift }) {
|
|
1782
|
+
export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck, skipWorktreeCheck, skipTechDocsCheck, skipCoverageCheck, allowSubToolDrift, noDeployPublic }) {
|
|
1649
1783
|
repoPath = repoPath || process.cwd();
|
|
1650
1784
|
const currentVersion = detectCurrentVersion(repoPath);
|
|
1651
1785
|
const newVersion = bumpSemver(currentVersion, level);
|
|
@@ -1667,9 +1801,17 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
1667
1801
|
}
|
|
1668
1802
|
}
|
|
1669
1803
|
|
|
1670
|
-
// 0. License compliance gate
|
|
1804
|
+
// 0. License compliance gate (MANDATORY: blocks release if .license-guard.json missing)
|
|
1671
1805
|
const configPath = join(repoPath, '.license-guard.json');
|
|
1672
|
-
if (existsSync(configPath)) {
|
|
1806
|
+
if (!existsSync(configPath)) {
|
|
1807
|
+
console.log(` ✗ .license-guard.json not found.`);
|
|
1808
|
+
console.log(` Every repo must have .license-guard.json to release.`);
|
|
1809
|
+
console.log(` Run: wip-repo-init (scaffolds ai/, .license-guard.json, CLA.md)`);
|
|
1810
|
+
console.log(` Or: wip-license-guard init`);
|
|
1811
|
+
console.log('');
|
|
1812
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
1813
|
+
}
|
|
1814
|
+
{
|
|
1673
1815
|
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
1674
1816
|
const licenseIssues = [];
|
|
1675
1817
|
|
|
@@ -1697,6 +1839,24 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
1697
1839
|
if (config.license === 'MIT+AGPL' && !readme.includes('AGPL')) licenseIssues.push('README.md License section missing AGPL reference');
|
|
1698
1840
|
}
|
|
1699
1841
|
|
|
1842
|
+
// .npmignore must exclude ai/ if repo has ai/ directory
|
|
1843
|
+
const aiDir = join(repoPath, 'ai');
|
|
1844
|
+
if (existsSync(aiDir)) {
|
|
1845
|
+
const npmignorePath = join(repoPath, '.npmignore');
|
|
1846
|
+
const pkgJson = JSON.parse(readFileSync(join(repoPath, 'package.json'), 'utf8'));
|
|
1847
|
+
const hasFilesWhitelist = Array.isArray(pkgJson.files);
|
|
1848
|
+
if (!hasFilesWhitelist) {
|
|
1849
|
+
if (!existsSync(npmignorePath)) {
|
|
1850
|
+
licenseIssues.push('.npmignore is missing (ai/ directory exists and could leak to npm)');
|
|
1851
|
+
} else {
|
|
1852
|
+
const npmignore = readFileSync(npmignorePath, 'utf8');
|
|
1853
|
+
if (!npmignore.includes('ai/')) {
|
|
1854
|
+
licenseIssues.push('.npmignore does not exclude ai/ (plans and bugs could leak to npm)');
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1700
1860
|
if (licenseIssues.length > 0) {
|
|
1701
1861
|
console.log(` ✗ License compliance failed:`);
|
|
1702
1862
|
for (const issue of licenseIssues) console.log(` - ${issue}`);
|
|
@@ -1984,6 +2144,14 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
1984
2144
|
writePackageVersion(repoPath, newVersion);
|
|
1985
2145
|
console.log(` ✓ package.json -> ${newVersion}`);
|
|
1986
2146
|
|
|
2147
|
+
// 1.25. Auto-bump sub-tool versions (Phase 5: auto-fix before validation)
|
|
2148
|
+
{
|
|
2149
|
+
const autoBumped = autoFixSubToolVersions(repoPath);
|
|
2150
|
+
if (autoBumped > 0) {
|
|
2151
|
+
console.log(` ✓ Auto-bumped ${autoBumped} sub-tool(s)`);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1987
2155
|
// 1.5. Validate sub-tool version bumps (Phase 8: error by default)
|
|
1988
2156
|
{
|
|
1989
2157
|
const subToolResult = validateSubToolVersions(repoPath, allowSubToolDrift);
|
|
@@ -2013,35 +2181,76 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
2013
2181
|
console.log(` ✓ Product docs synced to v${newVersion} (${docsUpdated} file(s))`);
|
|
2014
2182
|
}
|
|
2015
2183
|
|
|
2016
|
-
// 4. Git commit + tag
|
|
2017
|
-
gitCommitAndTag(repoPath, newVersion, notes);
|
|
2018
|
-
console.log(` ✓ Committed and tagged v${newVersion}`);
|
|
2019
|
-
|
|
2020
|
-
// 5. Push commit + tag (with auto-PR fallback on protected main, Phase 4)
|
|
2021
|
-
{
|
|
2022
|
-
const pushResult = pushReleaseWithAutoPr(repoPath, newVersion, level);
|
|
2023
|
-
if (pushResult.ok) {
|
|
2024
|
-
console.log(` ✓ Pushed to remote (${pushResult.via})`);
|
|
2025
|
-
} else {
|
|
2026
|
-
logPushFailure(pushResult, `v${newVersion}`);
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
2184
|
// Distribution results collector (#104)
|
|
2031
2185
|
const distResults = [];
|
|
2032
2186
|
|
|
2187
|
+
// 4. npm publish BEFORE commit (Phase 3: true publish-before-commit)
|
|
2188
|
+
// Files are bumped and staged but NOT committed. If npm fails, we just
|
|
2189
|
+
// revert the file changes. No commit, no tag, no remote state to clean up.
|
|
2033
2190
|
if (!noPublish) {
|
|
2034
|
-
// 6. npm publish
|
|
2035
2191
|
try {
|
|
2036
2192
|
publishNpm(repoPath);
|
|
2037
2193
|
const pkg = JSON.parse(readFileSync(join(repoPath, 'package.json'), 'utf8'));
|
|
2038
2194
|
distResults.push({ target: 'npm', status: 'ok', detail: `${pkg.name}@${newVersion}` });
|
|
2039
2195
|
console.log(` ✓ Published to npm`);
|
|
2040
2196
|
} catch (e) {
|
|
2041
|
-
distResults.push({ target: 'npm', status: 'failed', detail: e.message });
|
|
2042
2197
|
console.log(` ✗ npm publish failed: ${e.message}`);
|
|
2198
|
+
console.log(` Reverting file changes (no commit was made)...`);
|
|
2199
|
+
try {
|
|
2200
|
+
execSync('git checkout -- .', { cwd: repoPath, stdio: 'pipe' });
|
|
2201
|
+
// Clean up any new files (like trashed release notes)
|
|
2202
|
+
execSync('git clean -fd _trash/', { cwd: repoPath, stdio: 'pipe' });
|
|
2203
|
+
console.log(` ✓ Reverted. Working tree is clean. Fix the issue and try again.`);
|
|
2204
|
+
} catch (revertErr) {
|
|
2205
|
+
console.log(` ✗ Revert failed: ${revertErr.message}`);
|
|
2206
|
+
console.log(` Manual cleanup: git checkout -- .`);
|
|
2207
|
+
}
|
|
2208
|
+
console.log('');
|
|
2209
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2043
2210
|
}
|
|
2044
2211
|
|
|
2212
|
+
// Phase 5: Auto-publish changed sub-tools to npm
|
|
2213
|
+
const toolsDir = join(repoPath, 'tools');
|
|
2214
|
+
if (existsSync(toolsDir)) {
|
|
2215
|
+
for (const tool of readdirSync(toolsDir)) {
|
|
2216
|
+
const toolPath = join(toolsDir, tool);
|
|
2217
|
+
const toolPkg = join(toolPath, 'package.json');
|
|
2218
|
+
if (!existsSync(toolPkg)) continue;
|
|
2219
|
+
const pkg = JSON.parse(readFileSync(toolPkg, 'utf8'));
|
|
2220
|
+
if (!pkg.name || pkg.private) continue;
|
|
2221
|
+
try {
|
|
2222
|
+
publishNpm(toolPath);
|
|
2223
|
+
distResults.push({ target: 'npm', status: 'ok', detail: `${pkg.name}@${pkg.version}` });
|
|
2224
|
+
console.log(` ✓ Published sub-tool: ${pkg.name}@${pkg.version}`);
|
|
2225
|
+
} catch (e) {
|
|
2226
|
+
// Sub-tool publish failure is non-fatal but loud (Phase 7)
|
|
2227
|
+
const msg = e.message || '';
|
|
2228
|
+
if (msg.includes('previously published') || msg.includes('cannot publish over')) {
|
|
2229
|
+
// Already published at this version. Not an error.
|
|
2230
|
+
} else {
|
|
2231
|
+
distResults.push({ target: 'npm', status: 'failed', detail: `${pkg.name}: ${msg}` });
|
|
2232
|
+
console.log(` ✗ Sub-tool ${pkg.name} publish failed: ${msg}`);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// 5. Git commit + tag (AFTER npm publish succeeds)
|
|
2240
|
+
gitCommitAndTag(repoPath, newVersion, notes);
|
|
2241
|
+
console.log(` ✓ Committed and tagged v${newVersion}`);
|
|
2242
|
+
|
|
2243
|
+
// 5.5. Push commit + tag
|
|
2244
|
+
{
|
|
2245
|
+
const pushResult = pushReleaseWithAutoPr(repoPath, newVersion, level);
|
|
2246
|
+
if (pushResult.ok) {
|
|
2247
|
+
console.log(` ✓ Pushed to remote (${pushResult.via})`);
|
|
2248
|
+
} else {
|
|
2249
|
+
logPushFailure(pushResult, `v${newVersion}`);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
if (!noPublish) {
|
|
2045
2254
|
// 7. GitHub Packages ... SKIPPED from private repos.
|
|
2046
2255
|
// deploy-public.sh publishes to GitHub Packages from the public repo clone.
|
|
2047
2256
|
// Publishing from private ties the package to the private repo, making it
|
|
@@ -2288,6 +2497,29 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
2288
2497
|
}) + '\n');
|
|
2289
2498
|
} catch {}
|
|
2290
2499
|
|
|
2500
|
+
// 12. deploy-public: sync private -> public mirror (Phase 6)
|
|
2501
|
+
// Runs at the end of every stable release unless --no-deploy-public is set
|
|
2502
|
+
// or the repo has no deploy-public.sh script. Skips silently for non-
|
|
2503
|
+
// -private repos (no public mirror to sync).
|
|
2504
|
+
{
|
|
2505
|
+
const dp = runDeployPublic(repoPath, { skip: noDeployPublic });
|
|
2506
|
+
if (dp.skipped) {
|
|
2507
|
+
if (dp.reason === 'flag') {
|
|
2508
|
+
console.log(` - deploy-public: skipped (--no-deploy-public)`);
|
|
2509
|
+
} else if (dp.reason === 'no-script') {
|
|
2510
|
+
console.log(` - deploy-public: skipped (no tools/deploy-public/deploy-public.sh)`);
|
|
2511
|
+
} else if (dp.reason === 'not-private-repo') {
|
|
2512
|
+
console.log(` - deploy-public: skipped (origin is not a -private repo)`);
|
|
2513
|
+
}
|
|
2514
|
+
} else if (dp.ok) {
|
|
2515
|
+
console.log(` \u2713 deploy-public: synced to ${dp.publicRepo}`);
|
|
2516
|
+
} else {
|
|
2517
|
+
console.log(` \u2717 deploy-public failed for ${dp.publicRepo}`);
|
|
2518
|
+
if (dp.detail) console.log(` ${dp.detail.split('\n')[0]}`);
|
|
2519
|
+
console.log(` Manual recovery: bash tools/deploy-public/deploy-public.sh ${repoPath} ${dp.publicRepo}`);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2291
2523
|
console.log('');
|
|
2292
2524
|
console.log(` Done. ${repoName} v${newVersion} released.`);
|
|
2293
2525
|
console.log('');
|
|
@@ -2306,7 +2538,7 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
2306
2538
|
* No deploy-public. No code sync. No CHANGELOG gate. No product docs gate.
|
|
2307
2539
|
* Lightweight: bump version, npm publish with tag, optional GitHub prerelease.
|
|
2308
2540
|
*/
|
|
2309
|
-
export async function releasePrerelease({ repoPath, track, notes, dryRun, noPublish, publishReleaseNotes, skipWorktreeCheck, allowSubToolDrift }) {
|
|
2541
|
+
export async function releasePrerelease({ repoPath, track, notes, dryRun, noPublish, publishReleaseNotes, skipWorktreeCheck, allowSubToolDrift, noDeployPublic }) {
|
|
2310
2542
|
repoPath = repoPath || process.cwd();
|
|
2311
2543
|
const currentVersion = detectCurrentVersion(repoPath);
|
|
2312
2544
|
const newVersion = bumpPrerelease(currentVersion, track);
|
|
@@ -2330,6 +2562,66 @@ export async function releasePrerelease({ repoPath, track, notes, dryRun, noPubl
|
|
|
2330
2562
|
}
|
|
2331
2563
|
}
|
|
2332
2564
|
|
|
2565
|
+
// License compliance gate (MANDATORY: blocks release if .license-guard.json missing)
|
|
2566
|
+
{
|
|
2567
|
+
const configPath = join(repoPath, '.license-guard.json');
|
|
2568
|
+
if (!existsSync(configPath)) {
|
|
2569
|
+
console.log(` \u2717 .license-guard.json not found.`);
|
|
2570
|
+
console.log(` Every repo must have .license-guard.json to release.`);
|
|
2571
|
+
console.log(` Run: wip-repo-init (scaffolds ai/, .license-guard.json, CLA.md)`);
|
|
2572
|
+
console.log(` Or: wip-license-guard init`);
|
|
2573
|
+
console.log('');
|
|
2574
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2575
|
+
}
|
|
2576
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
2577
|
+
const licenseIssues = [];
|
|
2578
|
+
const licensePath = join(repoPath, 'LICENSE');
|
|
2579
|
+
if (!existsSync(licensePath)) {
|
|
2580
|
+
licenseIssues.push('LICENSE file is missing');
|
|
2581
|
+
} else {
|
|
2582
|
+
const licenseText = readFileSync(licensePath, 'utf8');
|
|
2583
|
+
if (!licenseText.includes(config.copyright)) {
|
|
2584
|
+
licenseIssues.push(`LICENSE copyright does not match "${config.copyright}"`);
|
|
2585
|
+
}
|
|
2586
|
+
if (config.license === 'MIT+AGPL' && !licenseText.includes('AGPL') && !licenseText.includes('GNU Affero')) {
|
|
2587
|
+
licenseIssues.push('LICENSE is MIT-only but config requires MIT+AGPL');
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
if (!existsSync(join(repoPath, 'CLA.md'))) {
|
|
2591
|
+
licenseIssues.push('CLA.md is missing');
|
|
2592
|
+
}
|
|
2593
|
+
const readmePath = join(repoPath, 'README.md');
|
|
2594
|
+
if (existsSync(readmePath)) {
|
|
2595
|
+
const readme = readFileSync(readmePath, 'utf8');
|
|
2596
|
+
if (!readme.includes('## License')) licenseIssues.push('README.md missing ## License section');
|
|
2597
|
+
if (config.license === 'MIT+AGPL' && !readme.includes('AGPL')) licenseIssues.push('README.md License section missing AGPL reference');
|
|
2598
|
+
}
|
|
2599
|
+
const aiDir = join(repoPath, 'ai');
|
|
2600
|
+
if (existsSync(aiDir)) {
|
|
2601
|
+
const npmignorePath = join(repoPath, '.npmignore');
|
|
2602
|
+
const pkgJson = JSON.parse(readFileSync(join(repoPath, 'package.json'), 'utf8'));
|
|
2603
|
+
const hasFilesWhitelist = Array.isArray(pkgJson.files);
|
|
2604
|
+
if (!hasFilesWhitelist) {
|
|
2605
|
+
if (!existsSync(npmignorePath)) {
|
|
2606
|
+
licenseIssues.push('.npmignore is missing (ai/ directory exists and could leak to npm)');
|
|
2607
|
+
} else {
|
|
2608
|
+
const npmignore = readFileSync(npmignorePath, 'utf8');
|
|
2609
|
+
if (!npmignore.includes('ai/')) {
|
|
2610
|
+
licenseIssues.push('.npmignore does not exclude ai/ (plans and bugs could leak to npm)');
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
if (licenseIssues.length > 0) {
|
|
2616
|
+
console.log(` \u2717 License compliance failed:`);
|
|
2617
|
+
for (const issue of licenseIssues) console.log(` - ${issue}`);
|
|
2618
|
+
console.log(`\n Run \`wip-license-guard check --fix\` to auto-repair, then try again.`);
|
|
2619
|
+
console.log('');
|
|
2620
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2621
|
+
}
|
|
2622
|
+
console.log(` \u2713 License compliance passed`);
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2333
2625
|
if (dryRun) {
|
|
2334
2626
|
console.log(` [dry run] Would bump package.json to ${newVersion}`);
|
|
2335
2627
|
if (!noPublish) {
|
|
@@ -2358,6 +2650,14 @@ export async function releasePrerelease({ repoPath, track, notes, dryRun, noPubl
|
|
|
2358
2650
|
writePackageVersion(repoPath, newVersion);
|
|
2359
2651
|
console.log(` \u2713 package.json -> ${newVersion}`);
|
|
2360
2652
|
|
|
2653
|
+
// 1.25. Auto-bump sub-tool versions (Phase 5)
|
|
2654
|
+
{
|
|
2655
|
+
const autoBumped = autoFixSubToolVersions(repoPath);
|
|
2656
|
+
if (autoBumped > 0) {
|
|
2657
|
+
console.log(` \u2713 Auto-bumped ${autoBumped} sub-tool(s)`);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2361
2661
|
// 1.5. Validate sub-tool version bumps (Phase 8: error by default)
|
|
2362
2662
|
{
|
|
2363
2663
|
const subToolResult = validateSubToolVersions(repoPath, allowSubToolDrift);
|
|
@@ -2405,6 +2705,31 @@ export async function releasePrerelease({ repoPath, track, notes, dryRun, noPubl
|
|
|
2405
2705
|
console.log(` \u2717 npm publish failed: ${e.message}`);
|
|
2406
2706
|
}
|
|
2407
2707
|
|
|
2708
|
+
// 5.5. Auto-publish changed sub-tools to npm (Phase 5)
|
|
2709
|
+
const toolsDir = join(repoPath, 'tools');
|
|
2710
|
+
if (existsSync(toolsDir)) {
|
|
2711
|
+
for (const tool of readdirSync(toolsDir)) {
|
|
2712
|
+
const toolPath = join(toolsDir, tool);
|
|
2713
|
+
const toolPkg = join(toolPath, 'package.json');
|
|
2714
|
+
if (!existsSync(toolPkg)) continue;
|
|
2715
|
+
const pkg = JSON.parse(readFileSync(toolPkg, 'utf8'));
|
|
2716
|
+
if (!pkg.name || pkg.private) continue;
|
|
2717
|
+
try {
|
|
2718
|
+
publishNpm(toolPath);
|
|
2719
|
+
distResults.push({ target: 'npm', status: 'ok', detail: `${pkg.name}@${pkg.version}` });
|
|
2720
|
+
console.log(` \u2713 Published sub-tool: ${pkg.name}@${pkg.version}`);
|
|
2721
|
+
} catch (e) {
|
|
2722
|
+
const msg = e.message || '';
|
|
2723
|
+
if (msg.includes('previously published') || msg.includes('cannot publish over')) {
|
|
2724
|
+
// Already published at this version. Not an error.
|
|
2725
|
+
} else {
|
|
2726
|
+
distResults.push({ target: 'npm', status: 'failed', detail: `${pkg.name}: ${msg}` });
|
|
2727
|
+
console.log(` \u2717 Sub-tool ${pkg.name} publish failed: ${msg}`);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2408
2733
|
// 6. GitHub prerelease on public repo (if opted in)
|
|
2409
2734
|
if (publishReleaseNotes) {
|
|
2410
2735
|
try {
|
|
@@ -2430,6 +2755,30 @@ export async function releasePrerelease({ repoPath, track, notes, dryRun, noPubl
|
|
|
2430
2755
|
}
|
|
2431
2756
|
}
|
|
2432
2757
|
|
|
2758
|
+
// deploy-public: sync private -> public mirror (Phase 6)
|
|
2759
|
+
// Beta: deploy to public (testing distribution pipeline)
|
|
2760
|
+
// Alpha: NEVER deploy to public (dev-only)
|
|
2761
|
+
if (track === 'alpha') {
|
|
2762
|
+
console.log(` - deploy-public: skipped (alpha is dev-only, never goes to public)`);
|
|
2763
|
+
} else {
|
|
2764
|
+
const dp = runDeployPublic(repoPath, { skip: noDeployPublic });
|
|
2765
|
+
if (dp.skipped) {
|
|
2766
|
+
if (dp.reason === 'flag') {
|
|
2767
|
+
console.log(` - deploy-public: skipped (--no-deploy-public)`);
|
|
2768
|
+
} else if (dp.reason === 'no-script') {
|
|
2769
|
+
console.log(` - deploy-public: skipped (no deploy-public.sh)`);
|
|
2770
|
+
} else if (dp.reason === 'not-private-repo') {
|
|
2771
|
+
console.log(` - deploy-public: skipped (origin is not a -private repo)`);
|
|
2772
|
+
}
|
|
2773
|
+
} else if (dp.ok) {
|
|
2774
|
+
console.log(` \u2713 deploy-public: synced to ${dp.publicRepo}`);
|
|
2775
|
+
} else {
|
|
2776
|
+
console.log(` \u2717 deploy-public failed for ${dp.publicRepo}`);
|
|
2777
|
+
if (dp.detail) console.log(` ${dp.detail.split('\n')[0]}`);
|
|
2778
|
+
console.log(` Manual recovery: bash tools/deploy-public/deploy-public.sh ${repoPath} ${dp.publicRepo}`);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2433
2782
|
console.log('');
|
|
2434
2783
|
console.log(` Done. ${repoName} v${newVersion} (${track}) released.`);
|
|
2435
2784
|
console.log('');
|
|
@@ -2470,9 +2819,17 @@ export async function releaseHotfix({ repoPath, notes, notesSource, dryRun, noPu
|
|
|
2470
2819
|
}
|
|
2471
2820
|
}
|
|
2472
2821
|
|
|
2473
|
-
// License compliance gate
|
|
2474
|
-
|
|
2475
|
-
|
|
2822
|
+
// License compliance gate (MANDATORY: blocks release if .license-guard.json missing)
|
|
2823
|
+
{
|
|
2824
|
+
const configPath = join(repoPath, '.license-guard.json');
|
|
2825
|
+
if (!existsSync(configPath)) {
|
|
2826
|
+
console.log(` \u2717 .license-guard.json not found.`);
|
|
2827
|
+
console.log(` Every repo must have .license-guard.json to release.`);
|
|
2828
|
+
console.log(` Run: wip-repo-init (scaffolds ai/, .license-guard.json, CLA.md)`);
|
|
2829
|
+
console.log(` Or: wip-license-guard init`);
|
|
2830
|
+
console.log('');
|
|
2831
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2832
|
+
}
|
|
2476
2833
|
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
2477
2834
|
const licenseIssues = [];
|
|
2478
2835
|
const licensePath = join(repoPath, 'LICENSE');
|
|
@@ -2484,6 +2841,9 @@ export async function releaseHotfix({ repoPath, notes, notesSource, dryRun, noPu
|
|
|
2484
2841
|
licenseIssues.push(`LICENSE copyright does not match "${config.copyright}"`);
|
|
2485
2842
|
}
|
|
2486
2843
|
}
|
|
2844
|
+
if (!existsSync(join(repoPath, 'CLA.md'))) {
|
|
2845
|
+
licenseIssues.push('CLA.md is missing');
|
|
2846
|
+
}
|
|
2487
2847
|
if (licenseIssues.length > 0) {
|
|
2488
2848
|
console.log(` \u2717 License compliance failed:`);
|
|
2489
2849
|
for (const issue of licenseIssues) console.log(` - ${issue}`);
|
package/package.json
CHANGED