@wipcomputer/wip-release 1.9.67 → 1.9.72
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/REFERENCE.md +154 -9
- package/SKILL.md +42 -3
- package/cli.js +94 -32
- package/core.mjs +702 -50
- package/package.json +1 -1
package/REFERENCE.md
CHANGED
|
@@ -3,15 +3,47 @@
|
|
|
3
3
|
|
|
4
4
|
Detailed usage, pipeline steps, flags, auth, and module API.
|
|
5
5
|
|
|
6
|
+
## Release Tracks
|
|
7
|
+
|
|
8
|
+
Four release tracks, each with different behavior:
|
|
9
|
+
|
|
10
|
+
| Track | npm tag | Public code sync | Public release notes | Default notes |
|
|
11
|
+
|-------|---------|-----------------|---------------------|---------------|
|
|
12
|
+
| Alpha | @alpha | No | No (opt in with --release-notes) | Silent |
|
|
13
|
+
| Beta | @beta | No | Yes, prerelease (opt out with --no-release-notes) | Visible |
|
|
14
|
+
| Hotfix | @latest | No | Yes (opt out with --no-release-notes) | Visible |
|
|
15
|
+
| Stable | @latest | Yes, full deploy-public | Yes, full notes | Full deploy |
|
|
16
|
+
|
|
17
|
+
### Version numbering
|
|
18
|
+
|
|
19
|
+
- Alpha: `1.9.68-alpha.1`, `1.9.68-alpha.2` (increments on repeat)
|
|
20
|
+
- Beta: `1.9.68-beta.1`, `1.9.68-beta.2` (increments on repeat)
|
|
21
|
+
- Hotfix: normal patch bump (`1.9.67` -> `1.9.68`)
|
|
22
|
+
- Stable: normal bump (patch/minor/major)
|
|
23
|
+
|
|
6
24
|
## Usage
|
|
7
25
|
|
|
8
26
|
Run from inside any repo:
|
|
9
27
|
|
|
10
28
|
```bash
|
|
29
|
+
# Stable (existing behavior)
|
|
11
30
|
wip-release patch # 1.0.0 -> 1.0.1
|
|
12
31
|
wip-release minor # 1.0.0 -> 1.1.0
|
|
13
32
|
wip-release major # 1.0.0 -> 2.0.0
|
|
14
33
|
|
|
34
|
+
# Alpha
|
|
35
|
+
wip-release alpha # 1.0.1-alpha.1 (npm @alpha, silent)
|
|
36
|
+
wip-release alpha --release-notes # 1.0.1-alpha.1 (npm @alpha + prerelease on public)
|
|
37
|
+
|
|
38
|
+
# Beta
|
|
39
|
+
wip-release beta # 1.0.1-beta.1 (npm @beta + prerelease on public)
|
|
40
|
+
wip-release beta --no-release-notes # 1.0.1-beta.1 (npm @beta, skip notes)
|
|
41
|
+
|
|
42
|
+
# Hotfix
|
|
43
|
+
wip-release hotfix # 1.0.0 -> 1.0.1 (npm @latest + release on public)
|
|
44
|
+
wip-release hotfix --no-release-notes # skip public release notes
|
|
45
|
+
|
|
46
|
+
# Common flags
|
|
15
47
|
wip-release patch --notes="fix auth config" # with changelog note
|
|
16
48
|
wip-release minor --dry-run # preview, no changes
|
|
17
49
|
wip-release patch --no-publish # bump + tag only
|
|
@@ -19,6 +51,8 @@ wip-release patch --no-publish # bump + tag only
|
|
|
19
51
|
|
|
20
52
|
## What It Does
|
|
21
53
|
|
|
54
|
+
### Stable pipeline
|
|
55
|
+
|
|
22
56
|
```
|
|
23
57
|
wip-grok: 1.0.0 -> 1.0.1 (patch)
|
|
24
58
|
────────────────────────────────────────
|
|
@@ -28,32 +62,97 @@ wip-release patch --no-publish # bump + tag only
|
|
|
28
62
|
✓ Committed and tagged v1.0.1
|
|
29
63
|
✓ Pushed to remote
|
|
30
64
|
✓ Published to npm
|
|
31
|
-
|
|
65
|
+
- GitHub Packages: handled by deploy-public.sh
|
|
32
66
|
✓ GitHub release v1.0.1 created
|
|
33
67
|
✓ Published to ClawHub
|
|
34
68
|
|
|
35
69
|
Done. wip-grok v1.0.1 released.
|
|
36
70
|
```
|
|
37
71
|
|
|
72
|
+
### Alpha/beta pipeline
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
wip-grok: 1.0.0 -> 1.0.1-alpha.1 (alpha)
|
|
76
|
+
────────────────────────────────────────
|
|
77
|
+
✓ package.json -> 1.0.1-alpha.1
|
|
78
|
+
✓ CHANGELOG.md updated
|
|
79
|
+
✓ Committed and tagged v1.0.1-alpha.1
|
|
80
|
+
✓ Pushed to remote
|
|
81
|
+
✓ Published to npm @alpha
|
|
82
|
+
- GitHub prerelease: skipped (silent alpha)
|
|
83
|
+
|
|
84
|
+
Done. wip-grok v1.0.1-alpha.1 (alpha) released.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Hotfix pipeline
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
wip-grok: 1.0.0 -> 1.0.1 (hotfix)
|
|
91
|
+
────────────────────────────────────────
|
|
92
|
+
✓ package.json -> 1.0.1
|
|
93
|
+
✓ SKILL.md -> 1.0.1
|
|
94
|
+
✓ CHANGELOG.md updated
|
|
95
|
+
✓ Committed and tagged v1.0.1
|
|
96
|
+
✓ Pushed to remote
|
|
97
|
+
✓ Published to npm @latest
|
|
98
|
+
✓ GitHub release v1.0.1 created on public repo
|
|
99
|
+
- deploy-public: skipped (hotfix)
|
|
100
|
+
|
|
101
|
+
Done. wip-grok v1.0.1 (hotfix) released.
|
|
102
|
+
```
|
|
103
|
+
|
|
38
104
|
## Pipeline Steps
|
|
39
105
|
|
|
106
|
+
### Stable (patch/minor/major)
|
|
107
|
+
|
|
40
108
|
1. **Bump `package.json`** ... patch, minor, or major
|
|
41
109
|
2. **Sync `SKILL.md`** ... updates version in YAML frontmatter (if file exists)
|
|
42
110
|
3. **Update `CHANGELOG.md`** ... prepends new version entry with date and notes
|
|
43
111
|
4. **Git commit + tag** ... commits changed files, creates `vX.Y.Z` tag
|
|
44
112
|
5. **Push** ... pushes commit and tag to remote
|
|
45
|
-
6. **npm publish** ... publishes to npmjs.com (auth via 1Password)
|
|
46
|
-
7. **GitHub Packages** ...
|
|
47
|
-
8. **GitHub release** ... creates release with changelog notes
|
|
113
|
+
6. **npm publish** ... publishes to npmjs.com with @latest (auth via 1Password)
|
|
114
|
+
7. **GitHub Packages** ... handled by deploy-public.sh
|
|
115
|
+
8. **GitHub release** ... creates release on private repo with changelog notes
|
|
48
116
|
9. **ClawHub publish** ... publishes skill to ClawHub (if SKILL.md exists)
|
|
117
|
+
10. **Branch cleanup** ... renames/prunes merged branches
|
|
118
|
+
11. **Worktree cleanup** ... prunes merged worktrees
|
|
119
|
+
|
|
120
|
+
### Alpha/Beta
|
|
121
|
+
|
|
122
|
+
1. **Bump `package.json`** ... adds prerelease suffix (-alpha.N / -beta.N)
|
|
123
|
+
2. **Update `CHANGELOG.md`** ... lightweight prerelease entry
|
|
124
|
+
3. **Git commit + tag** ... commits changed files, creates tag
|
|
125
|
+
4. **Push** ... pushes commit and tag to remote
|
|
126
|
+
5. **npm publish** ... publishes with --tag alpha or --tag beta
|
|
127
|
+
6. **GitHub prerelease** ... (beta: on by default, alpha: opt-in with --release-notes)
|
|
128
|
+
|
|
129
|
+
### Hotfix
|
|
130
|
+
|
|
131
|
+
1. **Bump `package.json`** ... patch bump (no suffix)
|
|
132
|
+
2. **Sync `SKILL.md`** ... updates version in YAML frontmatter
|
|
133
|
+
3. **Update `CHANGELOG.md`** ... prepends new version entry
|
|
134
|
+
4. **Git commit + tag** ... commits changed files, creates tag
|
|
135
|
+
5. **Push** ... pushes commit and tag to remote
|
|
136
|
+
6. **npm publish** ... publishes with --tag latest
|
|
137
|
+
7. **GitHub release** ... creates release on public repo (opt out with --no-release-notes)
|
|
138
|
+
8. **ClawHub publish** ... publishes skill to ClawHub (if SKILL.md exists)
|
|
139
|
+
9. **No deploy-public** ... code sync is skipped
|
|
49
140
|
|
|
50
141
|
## Flags
|
|
51
142
|
|
|
52
143
|
| Flag | What |
|
|
53
144
|
|------|------|
|
|
54
145
|
| `--notes="text"` | Changelog entry text |
|
|
146
|
+
| `--notes-file=path` | Read release narrative from a markdown file |
|
|
147
|
+
| `--release-notes` | Opt in to public release notes (alpha only) |
|
|
148
|
+
| `--no-release-notes` | Opt out of public release notes (beta, hotfix) |
|
|
55
149
|
| `--dry-run` | Show what would happen, change nothing |
|
|
56
150
|
| `--no-publish` | Bump + tag only, skip npm and GitHub release |
|
|
151
|
+
| `--skip-product-check` | Skip product docs gate (stable only) |
|
|
152
|
+
| `--skip-stale-check` | Skip stale remote branch check (stable only) |
|
|
153
|
+
| `--skip-worktree-check` | Skip worktree guard |
|
|
154
|
+
| `--skip-tech-docs-check` | Skip technical docs check (stable only) |
|
|
155
|
+
| `--skip-coverage-check` | Skip interface coverage check (stable only) |
|
|
57
156
|
|
|
58
157
|
## Auth
|
|
59
158
|
|
|
@@ -66,15 +165,26 @@ Requires:
|
|
|
66
165
|
- `gh` CLI authenticated (for GitHub Packages and releases)
|
|
67
166
|
- `clawhub` CLI authenticated (for ClawHub skill publishing)
|
|
68
167
|
|
|
168
|
+
## ldm install Integration
|
|
169
|
+
|
|
170
|
+
The installer checks different npm tags based on the track:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
ldm install # checks @latest (stable + hotfix)
|
|
174
|
+
ldm install --beta # checks @beta tag
|
|
175
|
+
ldm install --alpha # checks @alpha tag
|
|
176
|
+
```
|
|
177
|
+
|
|
69
178
|
## As a Module
|
|
70
179
|
|
|
71
180
|
```javascript
|
|
72
|
-
import { release, detectCurrentVersion, bumpSemver } from '@wipcomputer/wip-release';
|
|
181
|
+
import { release, releasePrerelease, releaseHotfix, detectCurrentVersion, bumpSemver, bumpPrerelease } from '@wipcomputer/wip-release';
|
|
73
182
|
|
|
74
183
|
const current = detectCurrentVersion('/path/to/repo');
|
|
75
184
|
const next = bumpSemver(current, 'minor');
|
|
76
185
|
console.log(`${current} -> ${next}`);
|
|
77
186
|
|
|
187
|
+
// Stable release
|
|
78
188
|
await release({
|
|
79
189
|
repoPath: '/path/to/repo',
|
|
80
190
|
level: 'patch',
|
|
@@ -82,19 +192,54 @@ await release({
|
|
|
82
192
|
dryRun: false,
|
|
83
193
|
noPublish: false,
|
|
84
194
|
});
|
|
195
|
+
|
|
196
|
+
// Alpha prerelease
|
|
197
|
+
await releasePrerelease({
|
|
198
|
+
repoPath: '/path/to/repo',
|
|
199
|
+
track: 'alpha',
|
|
200
|
+
notes: 'testing new feature',
|
|
201
|
+
dryRun: false,
|
|
202
|
+
noPublish: false,
|
|
203
|
+
publishReleaseNotes: false,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Beta prerelease
|
|
207
|
+
await releasePrerelease({
|
|
208
|
+
repoPath: '/path/to/repo',
|
|
209
|
+
track: 'beta',
|
|
210
|
+
notes: 'beta candidate',
|
|
211
|
+
dryRun: false,
|
|
212
|
+
noPublish: false,
|
|
213
|
+
publishReleaseNotes: true,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Hotfix
|
|
217
|
+
await releaseHotfix({
|
|
218
|
+
repoPath: '/path/to/repo',
|
|
219
|
+
notes: 'critical fix',
|
|
220
|
+
dryRun: false,
|
|
221
|
+
noPublish: false,
|
|
222
|
+
publishReleaseNotes: true,
|
|
223
|
+
});
|
|
85
224
|
```
|
|
86
225
|
|
|
87
226
|
## Exports
|
|
88
227
|
|
|
89
228
|
| Function | What |
|
|
90
229
|
|----------|------|
|
|
91
|
-
| `release({ repoPath, level, notes, dryRun, noPublish })` | Full pipeline |
|
|
230
|
+
| `release({ repoPath, level, notes, dryRun, noPublish })` | Full stable pipeline |
|
|
231
|
+
| `releasePrerelease({ repoPath, track, notes, dryRun, noPublish, publishReleaseNotes })` | Alpha/beta pipeline |
|
|
232
|
+
| `releaseHotfix({ repoPath, notes, dryRun, noPublish, publishReleaseNotes })` | Hotfix pipeline |
|
|
92
233
|
| `detectCurrentVersion(repoPath)` | Read version from package.json |
|
|
93
|
-
| `bumpSemver(version, level)` | Bump a semver string |
|
|
234
|
+
| `bumpSemver(version, level)` | Bump a semver string (patch/minor/major) |
|
|
235
|
+
| `bumpPrerelease(version, track)` | Bump a prerelease version (alpha/beta) |
|
|
94
236
|
| `syncSkillVersion(repoPath, newVersion)` | Update SKILL.md frontmatter |
|
|
95
237
|
| `updateChangelog(repoPath, newVersion, notes)` | Prepend to CHANGELOG.md |
|
|
96
|
-
| `publishNpm(repoPath)` | Publish to npmjs.com |
|
|
238
|
+
| `publishNpm(repoPath)` | Publish to npmjs.com (@latest) |
|
|
239
|
+
| `publishNpmWithTag(repoPath, tag)` | Publish to npmjs.com with specific tag |
|
|
97
240
|
| `publishGitHubPackages(repoPath)` | Publish to npm.pkg.github.com |
|
|
98
|
-
| `createGitHubRelease(repoPath, newVersion, notes, currentVersion)` | Create GitHub release
|
|
241
|
+
| `createGitHubRelease(repoPath, newVersion, notes, currentVersion)` | Create GitHub release on private repo |
|
|
242
|
+
| `createGitHubPrerelease(repoPath, newVersion, notes)` | Create GitHub prerelease on public repo |
|
|
243
|
+
| `createGitHubReleaseOnPublic(repoPath, newVersion, notes, currentVersion)` | Create GitHub release on public repo |
|
|
99
244
|
| `buildReleaseNotes(repoPath, currentVersion, newVersion, notes)` | Generate detailed release notes |
|
|
100
245
|
| `publishClawHub(repoPath, newVersion, notes)` | Publish skill to ClawHub |
|
package/SKILL.md
CHANGED
|
@@ -46,6 +46,8 @@ Local release pipeline. One command bumps version, updates all docs, publishes e
|
|
|
46
46
|
- Releasing a new version of any @wipcomputer package
|
|
47
47
|
- After merging a PR to main and you need to publish
|
|
48
48
|
- Bumping version + changelog + SKILL.md in one step
|
|
49
|
+
- Publishing alpha or beta prereleases for testing
|
|
50
|
+
- Pushing hotfixes to npm without full deploy-public
|
|
49
51
|
|
|
50
52
|
**Use --dry-run for:**
|
|
51
53
|
- Previewing what a release would do before committing
|
|
@@ -55,18 +57,48 @@ Local release pipeline. One command bumps version, updates all docs, publishes e
|
|
|
55
57
|
|
|
56
58
|
### Do NOT Use For
|
|
57
59
|
|
|
58
|
-
- Pre-release / alpha versions (not yet supported)
|
|
59
60
|
- Repos without a package.json
|
|
60
61
|
|
|
62
|
+
## Release Tracks
|
|
63
|
+
|
|
64
|
+
Four release tracks. Each has different behavior for npm tags, public repo sync, and release notes.
|
|
65
|
+
|
|
66
|
+
| Track | Command | npm tag | Public code sync | Public release notes |
|
|
67
|
+
|-------|---------|---------|-----------------|---------------------|
|
|
68
|
+
| Alpha | `wip-release alpha` | @alpha | No | No (opt in with --release-notes) |
|
|
69
|
+
| Beta | `wip-release beta` | @beta | No | Yes, prerelease (opt out with --no-release-notes) |
|
|
70
|
+
| Hotfix | `wip-release hotfix` | @latest | No | Yes (opt out with --no-release-notes) |
|
|
71
|
+
| Stable | `wip-release patch/minor/major` | @latest | Yes (deploy-public) | Yes, full notes |
|
|
72
|
+
|
|
73
|
+
### Version numbering
|
|
74
|
+
|
|
75
|
+
- Alpha: `1.9.68-alpha.1`, `1.9.68-alpha.2`, etc.
|
|
76
|
+
- Beta: `1.9.68-beta.1`, `1.9.68-beta.2`, etc.
|
|
77
|
+
- Hotfix: normal version bump (patch)
|
|
78
|
+
- Stable: normal version bump (patch/minor/major)
|
|
79
|
+
|
|
61
80
|
## API Reference
|
|
62
81
|
|
|
63
82
|
### CLI
|
|
64
83
|
|
|
65
84
|
```bash
|
|
66
|
-
|
|
85
|
+
# Stable (existing behavior)
|
|
86
|
+
wip-release patch # full pipeline
|
|
67
87
|
wip-release minor --dry-run # preview only
|
|
68
88
|
wip-release major --no-publish # bump + tag only
|
|
69
89
|
wip-release patch --skip-product-check # skip product docs gate
|
|
90
|
+
|
|
91
|
+
# Alpha
|
|
92
|
+
wip-release alpha # npm @alpha, no public notes
|
|
93
|
+
wip-release alpha --release-notes # npm @alpha + prerelease notes on public
|
|
94
|
+
|
|
95
|
+
# Beta
|
|
96
|
+
wip-release beta # npm @beta + prerelease notes on public
|
|
97
|
+
wip-release beta --no-release-notes # npm @beta, skip public notes
|
|
98
|
+
|
|
99
|
+
# Hotfix
|
|
100
|
+
wip-release hotfix # npm @latest + public release notes
|
|
101
|
+
wip-release hotfix --no-release-notes # npm @latest, skip public notes
|
|
70
102
|
```
|
|
71
103
|
|
|
72
104
|
### Product Docs Gate
|
|
@@ -108,9 +140,16 @@ After publishing, wip-release auto-copies SKILL.md to your website as plain text
|
|
|
108
140
|
### Module
|
|
109
141
|
|
|
110
142
|
```javascript
|
|
111
|
-
import { release, detectCurrentVersion, bumpSemver,
|
|
143
|
+
import { release, releasePrerelease, releaseHotfix, detectCurrentVersion, bumpSemver, bumpPrerelease } from '@wipcomputer/wip-release';
|
|
112
144
|
|
|
145
|
+
// Stable release
|
|
113
146
|
await release({ repoPath: '.', level: 'patch', notes: 'fix', dryRun: false, noPublish: false });
|
|
147
|
+
|
|
148
|
+
// Alpha/beta prerelease
|
|
149
|
+
await releasePrerelease({ repoPath: '.', track: 'alpha', notes: 'testing', dryRun: false, noPublish: false, publishReleaseNotes: false });
|
|
150
|
+
|
|
151
|
+
// Hotfix
|
|
152
|
+
await releaseHotfix({ repoPath: '.', notes: 'critical fix', dryRun: false, noPublish: false, publishReleaseNotes: true });
|
|
114
153
|
```
|
|
115
154
|
|
|
116
155
|
## Troubleshooting
|
package/cli.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Release tool CLI. Bumps version, updates docs, publishes.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { release, detectCurrentVersion, collectMergedPRNotes } from './core.mjs';
|
|
8
|
+
import { release, releasePrerelease, releaseHotfix, detectCurrentVersion, collectMergedPRNotes } from './core.mjs';
|
|
9
9
|
|
|
10
10
|
const args = process.argv.slice(2);
|
|
11
|
-
const level = args.find(a => ['patch', 'minor', 'major'].includes(a));
|
|
11
|
+
const level = args.find(a => ['patch', 'minor', 'major', 'alpha', 'beta', 'hotfix'].includes(a));
|
|
12
12
|
|
|
13
13
|
function flag(name) {
|
|
14
14
|
const prefix = `--${name}=`;
|
|
@@ -23,6 +23,9 @@ const skipStaleCheck = args.includes('--skip-stale-check');
|
|
|
23
23
|
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
|
+
const allowSubToolDrift = args.includes('--allow-sub-tool-drift');
|
|
27
|
+
const wantReleaseNotes = args.includes('--release-notes');
|
|
28
|
+
const noReleaseNotes = args.includes('--no-release-notes');
|
|
26
29
|
const notesFilePath = flag('notes-file');
|
|
27
30
|
let notes = flag('notes');
|
|
28
31
|
// Bug fix #121: use strict check, not truthiness. --notes="" is empty, not absent.
|
|
@@ -51,13 +54,15 @@ let notesSource = (notes !== null && notes !== undefined && notes !== '') ? 'fla
|
|
|
51
54
|
}
|
|
52
55
|
notes = readFileSync(resolved, 'utf8').trim();
|
|
53
56
|
notesSource = 'file';
|
|
54
|
-
} else if (level) {
|
|
57
|
+
} else if (level && ['patch', 'minor', 'major', 'hotfix'].includes(level)) {
|
|
55
58
|
// 2. Auto-detect RELEASE-NOTES-v{version}.md (ALWAYS checks, even if --notes provided)
|
|
59
|
+
// Only for stable levels and hotfix. Alpha/beta skip this.
|
|
56
60
|
try {
|
|
57
61
|
const { detectCurrentVersion, bumpSemver } = await import('./core.mjs');
|
|
58
62
|
const cwd = process.cwd();
|
|
59
63
|
const currentVersion = detectCurrentVersion(cwd);
|
|
60
|
-
const
|
|
64
|
+
const bumpLevel = level === 'hotfix' ? 'patch' : level;
|
|
65
|
+
const newVersion = bumpSemver(currentVersion, bumpLevel);
|
|
61
66
|
const dashed = newVersion.replace(/\./g, '-');
|
|
62
67
|
const autoFile = join(cwd, `RELEASE-NOTES-v${dashed}.md`);
|
|
63
68
|
if (existsSync(autoFile)) {
|
|
@@ -75,12 +80,13 @@ let notesSource = (notes !== null && notes !== undefined && notes !== '') ? 'fla
|
|
|
75
80
|
// 2.5. Auto-combine release notes from merged PRs since last tag (#237)
|
|
76
81
|
// Only runs when no single RELEASE-NOTES file was found on disk.
|
|
77
82
|
// Scans git merge history for RELEASE-NOTES files committed on PR branches.
|
|
78
|
-
if (level && notesSource !== 'file') {
|
|
83
|
+
if (level && ['patch', 'minor', 'major', 'hotfix'].includes(level) && notesSource !== 'file') {
|
|
79
84
|
try {
|
|
80
85
|
const { collectMergedPRNotes, detectCurrentVersion: dcv, bumpSemver: bs } = await import('./core.mjs');
|
|
81
86
|
const cwd = process.cwd();
|
|
82
87
|
const cv = dcv(cwd);
|
|
83
|
-
const
|
|
88
|
+
const bumpLevel = level === 'hotfix' ? 'patch' : level;
|
|
89
|
+
const nv = bs(cv, bumpLevel);
|
|
84
90
|
const combined = collectMergedPRNotes(cwd, cv, nv);
|
|
85
91
|
if (combined) {
|
|
86
92
|
if (flagNotes && flagNotes !== combined.notes) {
|
|
@@ -144,27 +150,38 @@ if (!level || args.includes('--help') || args.includes('-h')) {
|
|
|
144
150
|
console.log(`wip-release ... local release tool${current}
|
|
145
151
|
|
|
146
152
|
Usage:
|
|
147
|
-
wip-release patch 1.0.0 -> 1.0.1
|
|
148
|
-
wip-release minor 1.0.0 -> 1.1.0
|
|
149
|
-
wip-release major 1.0.0 -> 2.0.0
|
|
153
|
+
wip-release patch 1.0.0 -> 1.0.1 (stable)
|
|
154
|
+
wip-release minor 1.0.0 -> 1.1.0 (stable)
|
|
155
|
+
wip-release major 1.0.0 -> 2.0.0 (stable)
|
|
156
|
+
wip-release alpha 1.0.1-alpha.1 (prerelease)
|
|
157
|
+
wip-release beta 1.0.1-beta.1 (prerelease)
|
|
158
|
+
wip-release hotfix 1.0.0 -> 1.0.1 (hotfix, no deploy-public)
|
|
159
|
+
|
|
160
|
+
Release tracks:
|
|
161
|
+
alpha npm @alpha tag, no public notes (opt in with --release-notes)
|
|
162
|
+
beta npm @beta tag, prerelease notes on public (opt out with --no-release-notes)
|
|
163
|
+
hotfix npm @latest tag, release notes on public (opt out with --no-release-notes)
|
|
164
|
+
stable npm @latest tag, deploy-public, full notes (patch/minor/major)
|
|
150
165
|
|
|
151
166
|
Flags:
|
|
152
167
|
--notes="description" Release narrative (what was built and why)
|
|
153
168
|
--notes-file=path Read release narrative from a markdown file
|
|
169
|
+
--release-notes Opt in to public release notes (alpha only)
|
|
170
|
+
--no-release-notes Opt out of public release notes (beta, hotfix)
|
|
154
171
|
--dry-run Show what would happen, change nothing
|
|
155
172
|
--no-publish Bump + tag only, skip npm/GitHub
|
|
156
173
|
--skip-product-check Skip product docs check (dev update, roadmap, readme-first)
|
|
157
174
|
--skip-stale-check Skip stale remote branch check
|
|
158
|
-
--skip-worktree-check Skip worktree guard (
|
|
175
|
+
--skip-worktree-check Skip main-branch + worktree guard (break-glass only)
|
|
176
|
+
--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)
|
|
159
177
|
|
|
160
|
-
Release notes (REQUIRED
|
|
178
|
+
Release notes (REQUIRED for stable, optional for other tracks):
|
|
161
179
|
1. --notes-file=path Explicit file path
|
|
162
180
|
2. RELEASE-NOTES-v{ver}.md In repo root (auto-detected)
|
|
163
181
|
3. Merged PR notes Auto-combined from git history (#237)
|
|
164
182
|
4. ai/dev-updates/YYYY-MM-DD* Today's dev update (auto-detected)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
When batching multiple PRs, each PR's RELEASE-NOTES are auto-combined.
|
|
183
|
+
For stable releases: the --notes flag is NOT accepted. Write a file.
|
|
184
|
+
For alpha/beta/hotfix: --notes="text" is accepted as a convenience.
|
|
168
185
|
|
|
169
186
|
Skill publish to website:
|
|
170
187
|
Add .publish-skill.json to repo root: { "name": "my-tool" }
|
|
@@ -172,7 +189,7 @@ Skill publish to website:
|
|
|
172
189
|
After release, SKILL.md is copied to {website}/wip.computer/install/{name}.txt
|
|
173
190
|
and deploy.sh is run to push to VPS.
|
|
174
191
|
|
|
175
|
-
Pipeline:
|
|
192
|
+
Pipeline (stable):
|
|
176
193
|
1. Bump package.json version
|
|
177
194
|
2. Sync SKILL.md version (if exists)
|
|
178
195
|
3. Update CHANGELOG.md
|
|
@@ -181,23 +198,68 @@ Pipeline:
|
|
|
181
198
|
6. npm publish (via 1Password)
|
|
182
199
|
7. GitHub Packages publish
|
|
183
200
|
8. GitHub release create
|
|
184
|
-
9. Publish SKILL.md to website (if configured)
|
|
201
|
+
9. Publish SKILL.md to website (if configured)
|
|
202
|
+
|
|
203
|
+
Pipeline (alpha/beta):
|
|
204
|
+
1. Bump version with prerelease suffix (-alpha.N / -beta.N)
|
|
205
|
+
2. npm publish with --tag alpha or --tag beta
|
|
206
|
+
3. GitHub prerelease (beta default, alpha opt-in)
|
|
207
|
+
|
|
208
|
+
Pipeline (hotfix):
|
|
209
|
+
1. Bump patch version (no suffix)
|
|
210
|
+
2. npm publish with --tag latest
|
|
211
|
+
3. GitHub release (no deploy-public)`);
|
|
185
212
|
process.exit(level ? 0 : 1);
|
|
186
213
|
}
|
|
187
214
|
|
|
188
|
-
release
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}).catch(err => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
215
|
+
// Route to the correct release function based on track
|
|
216
|
+
if (level === 'alpha' || level === 'beta') {
|
|
217
|
+
// Prerelease track: alpha or beta
|
|
218
|
+
releasePrerelease({
|
|
219
|
+
repoPath: process.cwd(),
|
|
220
|
+
track: level,
|
|
221
|
+
notes,
|
|
222
|
+
dryRun,
|
|
223
|
+
noPublish,
|
|
224
|
+
publishReleaseNotes: level === 'alpha' ? wantReleaseNotes : !noReleaseNotes,
|
|
225
|
+
skipWorktreeCheck,
|
|
226
|
+
allowSubToolDrift,
|
|
227
|
+
}).catch(err => {
|
|
228
|
+
console.error(` \u2717 ${err.message}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
});
|
|
231
|
+
} else if (level === 'hotfix') {
|
|
232
|
+
// Hotfix track: patch bump, @latest tag, no deploy-public
|
|
233
|
+
releaseHotfix({
|
|
234
|
+
repoPath: process.cwd(),
|
|
235
|
+
notes,
|
|
236
|
+
notesSource,
|
|
237
|
+
dryRun,
|
|
238
|
+
noPublish,
|
|
239
|
+
publishReleaseNotes: !noReleaseNotes,
|
|
240
|
+
skipWorktreeCheck,
|
|
241
|
+
allowSubToolDrift,
|
|
242
|
+
}).catch(err => {
|
|
243
|
+
console.error(` \u2717 ${err.message}`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
});
|
|
246
|
+
} else {
|
|
247
|
+
// Stable track: patch, minor, major
|
|
248
|
+
release({
|
|
249
|
+
repoPath: process.cwd(),
|
|
250
|
+
level,
|
|
251
|
+
notes,
|
|
252
|
+
notesSource,
|
|
253
|
+
dryRun,
|
|
254
|
+
noPublish,
|
|
255
|
+
skipProductCheck,
|
|
256
|
+
skipStaleCheck,
|
|
257
|
+
skipWorktreeCheck,
|
|
258
|
+
skipTechDocsCheck,
|
|
259
|
+
skipCoverageCheck,
|
|
260
|
+
allowSubToolDrift,
|
|
261
|
+
}).catch(err => {
|
|
262
|
+
console.error(` \u2717 ${err.message}`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
});
|
|
265
|
+
}
|
package/core.mjs
CHANGED
|
@@ -25,7 +25,8 @@ export function detectCurrentVersion(repoPath) {
|
|
|
25
25
|
* Bump a semver string by level.
|
|
26
26
|
*/
|
|
27
27
|
export function bumpSemver(version, level) {
|
|
28
|
-
const
|
|
28
|
+
const base = version.replace(/-.*$/, '');
|
|
29
|
+
const [major, minor, patch] = base.split('.').map(Number);
|
|
29
30
|
switch (level) {
|
|
30
31
|
case 'major': return `${major + 1}.0.0`;
|
|
31
32
|
case 'minor': return `${major}.${minor + 1}.0`;
|
|
@@ -34,6 +35,37 @@ export function bumpSemver(version, level) {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Bump a version string for prerelease tracks (alpha, beta).
|
|
40
|
+
*
|
|
41
|
+
* If the current version already has the same prerelease prefix,
|
|
42
|
+
* increment the counter: 1.2.3-alpha.1 -> 1.2.3-alpha.2
|
|
43
|
+
*
|
|
44
|
+
* If the current version is a clean release or a different prerelease,
|
|
45
|
+
* bump patch and start at .1: 1.2.3 -> 1.2.4-alpha.1
|
|
46
|
+
*/
|
|
47
|
+
export function bumpPrerelease(version, track) {
|
|
48
|
+
// Check if current version already has this prerelease prefix
|
|
49
|
+
const preMatch = version.match(new RegExp(`^(\\d+\\.\\d+\\.\\d+)-${track}\\.(\\d+)$`));
|
|
50
|
+
if (preMatch) {
|
|
51
|
+
// Same track: increment the counter
|
|
52
|
+
const base = preMatch[1];
|
|
53
|
+
const counter = parseInt(preMatch[2], 10);
|
|
54
|
+
return `${base}-${track}.${counter + 1}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Strip any existing prerelease suffix to get the base version
|
|
58
|
+
const baseMatch = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
59
|
+
if (!baseMatch) throw new Error(`Cannot parse version: ${version}`);
|
|
60
|
+
|
|
61
|
+
const major = parseInt(baseMatch[1], 10);
|
|
62
|
+
const minor = parseInt(baseMatch[2], 10);
|
|
63
|
+
const patch = parseInt(baseMatch[3], 10);
|
|
64
|
+
|
|
65
|
+
// Bump patch and start prerelease at .1
|
|
66
|
+
return `${major}.${minor}.${patch + 1}-${track}.1`;
|
|
67
|
+
}
|
|
68
|
+
|
|
37
69
|
/**
|
|
38
70
|
* Write new version to package.json.
|
|
39
71
|
*/
|
|
@@ -200,6 +232,18 @@ export function publishNpm(repoPath) {
|
|
|
200
232
|
], { cwd: repoPath, stdio: 'inherit' });
|
|
201
233
|
}
|
|
202
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Publish to npm with a specific dist-tag (alpha, beta, latest).
|
|
237
|
+
*/
|
|
238
|
+
export function publishNpmWithTag(repoPath, tag) {
|
|
239
|
+
const token = getNpmToken();
|
|
240
|
+
execFileSync('npm', [
|
|
241
|
+
'publish', '--access', 'public',
|
|
242
|
+
'--tag', tag,
|
|
243
|
+
`--//registry.npmjs.org/:_authToken=${token}`
|
|
244
|
+
], { cwd: repoPath, stdio: 'inherit' });
|
|
245
|
+
}
|
|
246
|
+
|
|
203
247
|
/**
|
|
204
248
|
* Publish to GitHub Packages.
|
|
205
249
|
*/
|
|
@@ -1018,6 +1062,61 @@ export function createGitHubRelease(repoPath, newVersion, notes, currentVersion)
|
|
|
1018
1062
|
}
|
|
1019
1063
|
}
|
|
1020
1064
|
|
|
1065
|
+
/**
|
|
1066
|
+
* Create a GitHub prerelease on the PUBLIC repo (no code sync).
|
|
1067
|
+
* Used by alpha (opt-in) and beta (default) tracks.
|
|
1068
|
+
*/
|
|
1069
|
+
export function createGitHubPrerelease(repoPath, newVersion, notes) {
|
|
1070
|
+
const repoSlug = detectRepoSlug(repoPath);
|
|
1071
|
+
if (!repoSlug) throw new Error('Cannot detect repo slug from git remote');
|
|
1072
|
+
|
|
1073
|
+
// Target the public repo (strip -private suffix)
|
|
1074
|
+
const publicSlug = repoSlug.replace(/-private$/, '');
|
|
1075
|
+
const body = notes || `Prerelease ${newVersion}`;
|
|
1076
|
+
|
|
1077
|
+
const tmpFile = join(repoPath, '.release-notes-tmp.md');
|
|
1078
|
+
writeFileSync(tmpFile, body);
|
|
1079
|
+
|
|
1080
|
+
try {
|
|
1081
|
+
execFileSync('gh', [
|
|
1082
|
+
'release', 'create', `v${newVersion}`,
|
|
1083
|
+
'--title', `v${newVersion}`,
|
|
1084
|
+
'--notes-file', '.release-notes-tmp.md',
|
|
1085
|
+
'--prerelease',
|
|
1086
|
+
'--repo', publicSlug
|
|
1087
|
+
], { cwd: repoPath, stdio: 'inherit' });
|
|
1088
|
+
} finally {
|
|
1089
|
+
try { execFileSync('rm', ['-f', tmpFile]); } catch {}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Create a GitHub release on the PUBLIC repo (no code sync).
|
|
1095
|
+
* Used by the hotfix track.
|
|
1096
|
+
*/
|
|
1097
|
+
export function createGitHubReleaseOnPublic(repoPath, newVersion, notes, currentVersion) {
|
|
1098
|
+
const repoSlug = detectRepoSlug(repoPath);
|
|
1099
|
+
if (!repoSlug) throw new Error('Cannot detect repo slug from git remote');
|
|
1100
|
+
|
|
1101
|
+
// Target the public repo (strip -private suffix)
|
|
1102
|
+
const publicSlug = repoSlug.replace(/-private$/, '');
|
|
1103
|
+
const body = buildReleaseNotes(repoPath, currentVersion, newVersion, notes);
|
|
1104
|
+
|
|
1105
|
+
const tmpFile = join(repoPath, '.release-notes-tmp.md');
|
|
1106
|
+
writeFileSync(tmpFile, body);
|
|
1107
|
+
|
|
1108
|
+
try {
|
|
1109
|
+
execFileSync('gh', [
|
|
1110
|
+
'release', 'create', `v${newVersion}`,
|
|
1111
|
+
'--title', `v${newVersion}`,
|
|
1112
|
+
'--notes-file', '.release-notes-tmp.md',
|
|
1113
|
+
'--repo', publicSlug
|
|
1114
|
+
], { cwd: repoPath, stdio: 'inherit' });
|
|
1115
|
+
} finally {
|
|
1116
|
+
try { execFileSync('rm', ['-f', tmpFile]); } catch {}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1021
1120
|
/**
|
|
1022
1121
|
* Publish skill to ClawHub.
|
|
1023
1122
|
*/
|
|
@@ -1224,10 +1323,210 @@ export function checkStaleBranches(repoPath, level) {
|
|
|
1224
1323
|
|
|
1225
1324
|
// ── Main ────────────────────────────────────────────────────────────
|
|
1226
1325
|
|
|
1326
|
+
/**
|
|
1327
|
+
* Guard: wip-release must run from the main working tree on the main/master branch.
|
|
1328
|
+
*
|
|
1329
|
+
* Two independent conditions are enforced:
|
|
1330
|
+
*
|
|
1331
|
+
* 1. Linked worktree check: `git rev-parse --git-dir` of a linked worktree
|
|
1332
|
+
* resolves to a path under `.git/worktrees/...`. If we see that, the caller
|
|
1333
|
+
* is inside a feature worktree and must switch to the main working tree.
|
|
1334
|
+
* 2. Current branch check: even from the main working tree, `git branch
|
|
1335
|
+
* --show-current` must be `main` or `master`. If a user checked out a feature
|
|
1336
|
+
* branch in the main tree, the release would commit to the wrong branch.
|
|
1337
|
+
*
|
|
1338
|
+
* Both conditions bypassable via `--skip-worktree-check` for break-glass scenarios.
|
|
1339
|
+
*
|
|
1340
|
+
* Returns `{ ok: true }` on pass, or `{ ok: false, reason, currentPath, mainPath, branch }`
|
|
1341
|
+
* on fail so the caller can log and return the standard `{ failed: true }` shape.
|
|
1342
|
+
*
|
|
1343
|
+
* Related: `ai/product/bugs/guard/2026-04-05--cc-mini--guard-master-plan.md` Phase 3,
|
|
1344
|
+
* `ai/product/bugs/release-pipeline/2026-04-05--cc-mini--release-pipeline-master-plan.md`
|
|
1345
|
+
* Phase 1. Earlier today a wip-release alpha ran from a worktree branch because
|
|
1346
|
+
* `releasePrerelease` had no worktree check at all and the other two checks did
|
|
1347
|
+
* not cover the "main tree but non-main branch" case. This helper closes both gaps.
|
|
1348
|
+
*/
|
|
1349
|
+
function enforceMainBranchGuard(repoPath, skipWorktreeCheck) {
|
|
1350
|
+
if (skipWorktreeCheck) {
|
|
1351
|
+
return { ok: true, skipped: true };
|
|
1352
|
+
}
|
|
1353
|
+
try {
|
|
1354
|
+
const gitDir = execFileSync('git', ['rev-parse', '--git-dir'], {
|
|
1355
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1356
|
+
}).trim();
|
|
1357
|
+
if (gitDir.includes('/worktrees/')) {
|
|
1358
|
+
const worktreeList = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
1359
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1360
|
+
});
|
|
1361
|
+
const mainWorktree = worktreeList.split('\n')
|
|
1362
|
+
.find(line => line.startsWith('worktree '));
|
|
1363
|
+
const mainPath = mainWorktree ? mainWorktree.replace('worktree ', '') : '(unknown)';
|
|
1364
|
+
return {
|
|
1365
|
+
ok: false,
|
|
1366
|
+
reason: 'linked-worktree',
|
|
1367
|
+
currentPath: repoPath,
|
|
1368
|
+
mainPath,
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
const branch = execFileSync('git', ['branch', '--show-current'], {
|
|
1372
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1373
|
+
}).trim();
|
|
1374
|
+
if (branch && branch !== 'main' && branch !== 'master') {
|
|
1375
|
+
return {
|
|
1376
|
+
ok: false,
|
|
1377
|
+
reason: 'non-main-branch',
|
|
1378
|
+
currentPath: repoPath,
|
|
1379
|
+
branch,
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
return { ok: true, branch };
|
|
1383
|
+
} catch {
|
|
1384
|
+
// Git command failed: skip check gracefully so release can still run
|
|
1385
|
+
// in CI or unusual environments where git plumbing is restricted.
|
|
1386
|
+
return { ok: true, skipped: true };
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* Validate that sub-tool package.json versions were bumped when their files changed.
|
|
1392
|
+
*
|
|
1393
|
+
* Scans `tools/*\/package.json` in monorepo-style toolboxes. For each sub-tool
|
|
1394
|
+
* whose files changed since the last git tag, verifies the package.json version
|
|
1395
|
+
* differs from the version at that tag. If not, this used to be a WARNING that
|
|
1396
|
+
* let the release proceed, which shipped at least one "committed but never
|
|
1397
|
+
* deployed" bug earlier today (guard 1.9.71 had new code but the same version,
|
|
1398
|
+
* so ldm install ignored the sub-tool on redeploy).
|
|
1399
|
+
*
|
|
1400
|
+
* Phase 8 of the release-pipeline master plan: WARNING becomes ERROR by default.
|
|
1401
|
+
* Callers who genuinely want to proceed without bumping (e.g., a release that
|
|
1402
|
+
* touches sub-tool files in a non-shipping way like CI config) pass
|
|
1403
|
+
* `allowSubToolDrift: true`.
|
|
1404
|
+
*
|
|
1405
|
+
* Returns `{ ok: true }` on pass, `{ ok: false }` if any sub-tool drift was
|
|
1406
|
+
* detected without the allow flag.
|
|
1407
|
+
*
|
|
1408
|
+
* Related: `ai/product/bugs/release-pipeline/2026-04-05--cc-mini--release-pipeline-master-plan.md`
|
|
1409
|
+
* Phase 8.
|
|
1410
|
+
*/
|
|
1411
|
+
function validateSubToolVersions(repoPath, allowSubToolDrift) {
|
|
1412
|
+
const toolsDir = join(repoPath, 'tools');
|
|
1413
|
+
if (!existsSync(toolsDir)) {
|
|
1414
|
+
return { ok: true };
|
|
1415
|
+
}
|
|
1416
|
+
let lastTag = null;
|
|
1417
|
+
try {
|
|
1418
|
+
lastTag = execFileSync('git', ['describe', '--tags', '--abbrev=0'], {
|
|
1419
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1420
|
+
}).trim();
|
|
1421
|
+
} catch {
|
|
1422
|
+
return { ok: true }; // No prior tag, nothing to compare against
|
|
1423
|
+
}
|
|
1424
|
+
if (!lastTag) return { ok: true };
|
|
1425
|
+
|
|
1426
|
+
let driftDetected = false;
|
|
1427
|
+
let entries;
|
|
1428
|
+
try {
|
|
1429
|
+
entries = readdirSync(toolsDir, { withFileTypes: true });
|
|
1430
|
+
} catch {
|
|
1431
|
+
return { ok: true };
|
|
1432
|
+
}
|
|
1433
|
+
for (const entry of entries) {
|
|
1434
|
+
if (!entry.isDirectory()) continue;
|
|
1435
|
+
const subDir = join('tools', entry.name);
|
|
1436
|
+
const subPkgPath = join(toolsDir, entry.name, 'package.json');
|
|
1437
|
+
if (!existsSync(subPkgPath)) continue;
|
|
1438
|
+
try {
|
|
1439
|
+
const diff = execFileSync('git', ['diff', '--name-only', lastTag, 'HEAD', '--', subDir], {
|
|
1440
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1441
|
+
}).trim();
|
|
1442
|
+
if (!diff) continue;
|
|
1443
|
+
const currentSubVersion = JSON.parse(readFileSync(subPkgPath, 'utf8')).version;
|
|
1444
|
+
let oldSubVersion = null;
|
|
1445
|
+
try {
|
|
1446
|
+
oldSubVersion = JSON.parse(
|
|
1447
|
+
execFileSync('git', ['show', `${lastTag}:${subDir}/package.json`], {
|
|
1448
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1449
|
+
})
|
|
1450
|
+
).version;
|
|
1451
|
+
} catch {}
|
|
1452
|
+
if (currentSubVersion === oldSubVersion) {
|
|
1453
|
+
if (allowSubToolDrift) {
|
|
1454
|
+
console.log(` ! WARNING (allowed by --allow-sub-tool-drift): ${entry.name} has changed files since ${lastTag} but version is still ${currentSubVersion}`);
|
|
1455
|
+
console.log(` Changed: ${diff.split('\n').join(', ')}`);
|
|
1456
|
+
} else {
|
|
1457
|
+
console.log(` \u2717 ${entry.name} has changed files since ${lastTag} but tools/${entry.name}/package.json version is still ${currentSubVersion}.`);
|
|
1458
|
+
console.log(` Changed: ${diff.split('\n').join(', ')}`);
|
|
1459
|
+
console.log(` Bump tools/${entry.name}/package.json before releasing, or pass --allow-sub-tool-drift to override.`);
|
|
1460
|
+
console.log('');
|
|
1461
|
+
driftDetected = true;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
} catch {}
|
|
1465
|
+
}
|
|
1466
|
+
return { ok: !driftDetected };
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Pre-tag collision check. Returns `{ ok: true }` if no collision, otherwise
|
|
1471
|
+
* `{ ok: false, tag }` with a message logged. Phase 2 of the release-pipeline
|
|
1472
|
+
* master plan: earlier today `wip-release alpha` failed mid-pipeline because
|
|
1473
|
+
* `v1.9.71-alpha.4` and `v1.9.71-alpha.5` existed as local-only tags from
|
|
1474
|
+
* prior failed releases. The release tool has no recovery path; this helper
|
|
1475
|
+
* catches the collision before the bump+commit happens, so the user gets a
|
|
1476
|
+
* clear error and concrete recovery command instead of a mid-pipeline failure.
|
|
1477
|
+
*/
|
|
1478
|
+
function checkTagCollision(repoPath, newVersion) {
|
|
1479
|
+
const tag = `v${newVersion}`;
|
|
1480
|
+
try {
|
|
1481
|
+
const localTags = execFileSync('git', ['tag', '-l', tag], {
|
|
1482
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1483
|
+
}).trim();
|
|
1484
|
+
if (localTags === tag) {
|
|
1485
|
+
// Tag exists locally. Is it also on remote?
|
|
1486
|
+
try {
|
|
1487
|
+
const remoteTags = execFileSync('git', ['ls-remote', '--tags', 'origin', tag], {
|
|
1488
|
+
cwd: repoPath, encoding: 'utf8'
|
|
1489
|
+
}).trim();
|
|
1490
|
+
if (remoteTags.includes(tag)) {
|
|
1491
|
+
// Tag is on remote: legitimate prior release. Refuse.
|
|
1492
|
+
console.log(` \u2717 Tag ${tag} already exists on origin (prior release).`);
|
|
1493
|
+
console.log(` Bump the version manually in package.json or run with a different level.`);
|
|
1494
|
+
console.log('');
|
|
1495
|
+
return { ok: false, tag, reason: 'on-remote' };
|
|
1496
|
+
}
|
|
1497
|
+
} catch {}
|
|
1498
|
+
// Tag exists locally but NOT on remote: stale leftover from a failed release.
|
|
1499
|
+
// Refuse with a concrete recovery command so the user knows this is safe to clean up.
|
|
1500
|
+
console.log(` \u2717 Tag ${tag} exists locally but not on origin (stale leftover from a prior failed release).`);
|
|
1501
|
+
console.log(` Safe to delete because it was never pushed. Recover with:`);
|
|
1502
|
+
console.log(` git tag -d ${tag} && wip-release <track>`);
|
|
1503
|
+
console.log('');
|
|
1504
|
+
return { ok: false, tag, reason: 'stale-local' };
|
|
1505
|
+
}
|
|
1506
|
+
} catch {}
|
|
1507
|
+
return { ok: true };
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function logMainBranchGuardFailure(result) {
|
|
1511
|
+
if (result.reason === 'linked-worktree') {
|
|
1512
|
+
console.log(` \u2717 wip-release must run from the main working tree, not a worktree.`);
|
|
1513
|
+
console.log(` Current: ${result.currentPath}`);
|
|
1514
|
+
console.log(` Main working tree: ${result.mainPath}`);
|
|
1515
|
+
console.log(` Switch to the main working tree and run again:`);
|
|
1516
|
+
console.log(` cd ${result.mainPath} && wip-release <track>`);
|
|
1517
|
+
} else if (result.reason === 'non-main-branch') {
|
|
1518
|
+
console.log(` \u2717 wip-release must run on the main branch, not a feature branch.`);
|
|
1519
|
+
console.log(` Current branch: ${result.branch}`);
|
|
1520
|
+
console.log(` Switch to main and pull latest:`);
|
|
1521
|
+
console.log(` git checkout main && git pull && wip-release <track>`);
|
|
1522
|
+
}
|
|
1523
|
+
console.log('');
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1227
1526
|
/**
|
|
1228
1527
|
* Run the full release pipeline.
|
|
1229
1528
|
*/
|
|
1230
|
-
export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck, skipWorktreeCheck, skipTechDocsCheck, skipCoverageCheck }) {
|
|
1529
|
+
export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck, skipWorktreeCheck, skipTechDocsCheck, skipCoverageCheck, allowSubToolDrift }) {
|
|
1231
1530
|
repoPath = repoPath || process.cwd();
|
|
1232
1531
|
const currentVersion = detectCurrentVersion(repoPath);
|
|
1233
1532
|
const newVersion = bumpSemver(currentVersion, level);
|
|
@@ -1237,33 +1536,15 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
1237
1536
|
console.log(` ${repoName}: ${currentVersion} -> ${newVersion} (${level})`);
|
|
1238
1537
|
console.log(` ${'─'.repeat(40)}`);
|
|
1239
1538
|
|
|
1240
|
-
// -1.
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
// Get the main working tree path from `git worktree list`
|
|
1250
|
-
const worktreeList = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
1251
|
-
cwd: repoPath, encoding: 'utf8'
|
|
1252
|
-
});
|
|
1253
|
-
const mainWorktree = worktreeList.split('\n')
|
|
1254
|
-
.find(line => line.startsWith('worktree '));
|
|
1255
|
-
const mainPath = mainWorktree ? mainWorktree.replace('worktree ', '') : '(unknown)';
|
|
1256
|
-
|
|
1257
|
-
console.log(` \u2717 wip-release must run from the main working tree, not a worktree.`);
|
|
1258
|
-
console.log(` Current: ${repoPath}`);
|
|
1259
|
-
console.log(` Main working tree: ${mainPath}`);
|
|
1260
|
-
console.log(` Switch to the main working tree and run again.`);
|
|
1261
|
-
console.log('');
|
|
1262
|
-
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
1263
|
-
}
|
|
1264
|
-
console.log(' \u2713 Running from main working tree');
|
|
1265
|
-
} catch {
|
|
1266
|
-
// Git command failed... skip check gracefully
|
|
1539
|
+
// -1. Main-branch guard: block releases from linked worktrees or non-main branches
|
|
1540
|
+
{
|
|
1541
|
+
const guardResult = enforceMainBranchGuard(repoPath, skipWorktreeCheck);
|
|
1542
|
+
if (!guardResult.ok) {
|
|
1543
|
+
logMainBranchGuardFailure(guardResult);
|
|
1544
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
1545
|
+
}
|
|
1546
|
+
if (!guardResult.skipped) {
|
|
1547
|
+
console.log(` \u2713 Running from main working tree on ${guardResult.branch ?? 'main'}`);
|
|
1267
1548
|
}
|
|
1268
1549
|
}
|
|
1269
1550
|
|
|
@@ -1572,31 +1853,23 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
1572
1853
|
return { currentVersion, newVersion, dryRun: true };
|
|
1573
1854
|
}
|
|
1574
1855
|
|
|
1856
|
+
// 1.25. Pre-bump tag collision check (Phase 2).
|
|
1857
|
+
{
|
|
1858
|
+
const collision = checkTagCollision(repoPath, newVersion);
|
|
1859
|
+
if (!collision.ok) {
|
|
1860
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1575
1864
|
// 1. Bump package.json
|
|
1576
1865
|
writePackageVersion(repoPath, newVersion);
|
|
1577
1866
|
console.log(` ✓ package.json -> ${newVersion}`);
|
|
1578
1867
|
|
|
1579
|
-
// 1.5.
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
const entries = readdirSync(toolsDir, { withFileTypes: true });
|
|
1585
|
-
for (const entry of entries) {
|
|
1586
|
-
if (!entry.isDirectory()) continue;
|
|
1587
|
-
const subPkgPath = join(toolsDir, entry.name, 'package.json');
|
|
1588
|
-
if (existsSync(subPkgPath)) {
|
|
1589
|
-
try {
|
|
1590
|
-
const subPkg = JSON.parse(readFileSync(subPkgPath, 'utf8'));
|
|
1591
|
-
subPkg.version = newVersion;
|
|
1592
|
-
writeFileSync(subPkgPath, JSON.stringify(subPkg, null, 2) + '\n');
|
|
1593
|
-
subBumped++;
|
|
1594
|
-
} catch {}
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
} catch {}
|
|
1598
|
-
if (subBumped > 0) {
|
|
1599
|
-
console.log(` ✓ ${subBumped} sub-tool(s) -> ${newVersion}`);
|
|
1868
|
+
// 1.5. Validate sub-tool version bumps (Phase 8: error by default)
|
|
1869
|
+
{
|
|
1870
|
+
const subToolResult = validateSubToolVersions(repoPath, allowSubToolDrift);
|
|
1871
|
+
if (!subToolResult.ok) {
|
|
1872
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
1600
1873
|
}
|
|
1601
1874
|
}
|
|
1602
1875
|
|
|
@@ -1900,3 +2173,382 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
1900
2173
|
|
|
1901
2174
|
return { currentVersion, newVersion, dryRun: false };
|
|
1902
2175
|
}
|
|
2176
|
+
|
|
2177
|
+
// ── Prerelease Track (Alpha / Beta) ────────────────────────────────
|
|
2178
|
+
|
|
2179
|
+
/**
|
|
2180
|
+
* Release an alpha or beta prerelease.
|
|
2181
|
+
*
|
|
2182
|
+
* Alpha: npm @alpha, no public release notes by default (opt in with publishReleaseNotes).
|
|
2183
|
+
* Beta: npm @beta, prerelease notes on public repo by default (opt out with publishReleaseNotes=false).
|
|
2184
|
+
*
|
|
2185
|
+
* No deploy-public. No code sync. No CHANGELOG gate. No product docs gate.
|
|
2186
|
+
* Lightweight: bump version, npm publish with tag, optional GitHub prerelease.
|
|
2187
|
+
*/
|
|
2188
|
+
export async function releasePrerelease({ repoPath, track, notes, dryRun, noPublish, publishReleaseNotes, skipWorktreeCheck, allowSubToolDrift }) {
|
|
2189
|
+
repoPath = repoPath || process.cwd();
|
|
2190
|
+
const currentVersion = detectCurrentVersion(repoPath);
|
|
2191
|
+
const newVersion = bumpPrerelease(currentVersion, track);
|
|
2192
|
+
const repoName = basename(repoPath);
|
|
2193
|
+
|
|
2194
|
+
console.log('');
|
|
2195
|
+
console.log(` ${repoName}: ${currentVersion} -> ${newVersion} (${track})`);
|
|
2196
|
+
console.log(` ${'─'.repeat(40)}`);
|
|
2197
|
+
|
|
2198
|
+
// Main-branch guard: worktree + non-main branch check via shared helper.
|
|
2199
|
+
// Runs before the dry-run short-circuit so preview output from a feature
|
|
2200
|
+
// branch still refuses instead of printing a misleading "would bump" plan.
|
|
2201
|
+
{
|
|
2202
|
+
const guardResult = enforceMainBranchGuard(repoPath, skipWorktreeCheck);
|
|
2203
|
+
if (!guardResult.ok) {
|
|
2204
|
+
logMainBranchGuardFailure(guardResult);
|
|
2205
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2206
|
+
}
|
|
2207
|
+
if (!guardResult.skipped) {
|
|
2208
|
+
console.log(` \u2713 Running from main working tree on ${guardResult.branch ?? 'main'}`);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
if (dryRun) {
|
|
2213
|
+
console.log(` [dry run] Would bump package.json to ${newVersion}`);
|
|
2214
|
+
if (!noPublish) {
|
|
2215
|
+
console.log(` [dry run] Would npm publish with --tag ${track}`);
|
|
2216
|
+
if (publishReleaseNotes) {
|
|
2217
|
+
console.log(` [dry run] Would create GitHub prerelease v${newVersion} on public repo`);
|
|
2218
|
+
} else {
|
|
2219
|
+
console.log(` [dry run] No GitHub prerelease (silent)`);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
console.log('');
|
|
2223
|
+
console.log(` Dry run complete. No changes made.`);
|
|
2224
|
+
console.log('');
|
|
2225
|
+
return { currentVersion, newVersion, dryRun: true };
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
// 1.25. Pre-bump tag collision check (Phase 2).
|
|
2229
|
+
{
|
|
2230
|
+
const collision = checkTagCollision(repoPath, newVersion);
|
|
2231
|
+
if (!collision.ok) {
|
|
2232
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// 1. Bump package.json
|
|
2237
|
+
writePackageVersion(repoPath, newVersion);
|
|
2238
|
+
console.log(` \u2713 package.json -> ${newVersion}`);
|
|
2239
|
+
|
|
2240
|
+
// 1.5. Validate sub-tool version bumps (Phase 8: error by default)
|
|
2241
|
+
{
|
|
2242
|
+
const subToolResult = validateSubToolVersions(repoPath, allowSubToolDrift);
|
|
2243
|
+
if (!subToolResult.ok) {
|
|
2244
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// 2. Update CHANGELOG.md (lightweight entry)
|
|
2249
|
+
updateChangelog(repoPath, newVersion, notes || `${track} prerelease`);
|
|
2250
|
+
console.log(` \u2713 CHANGELOG.md updated`);
|
|
2251
|
+
|
|
2252
|
+
// 3. Git commit + tag
|
|
2253
|
+
const msg = `v${newVersion}: ${track} prerelease`;
|
|
2254
|
+
for (const f of ['package.json', 'CHANGELOG.md']) {
|
|
2255
|
+
if (existsSync(join(repoPath, f))) {
|
|
2256
|
+
execFileSync('git', ['add', f], { cwd: repoPath, stdio: 'pipe' });
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
execFileSync('git', ['commit', '--no-verify', '-m', msg], { cwd: repoPath, stdio: 'pipe' });
|
|
2260
|
+
execFileSync('git', ['tag', `v${newVersion}`], { cwd: repoPath, stdio: 'pipe' });
|
|
2261
|
+
console.log(` \u2713 Committed and tagged v${newVersion}`);
|
|
2262
|
+
|
|
2263
|
+
// 4. Push commit + tag
|
|
2264
|
+
try {
|
|
2265
|
+
execSync('git push && git push --tags', { cwd: repoPath, stdio: 'pipe' });
|
|
2266
|
+
console.log(` \u2713 Pushed to remote`);
|
|
2267
|
+
} catch {
|
|
2268
|
+
console.log(` ! Push failed. Push manually.`);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
const distResults = [];
|
|
2272
|
+
|
|
2273
|
+
if (!noPublish) {
|
|
2274
|
+
// 5. npm publish with dist-tag
|
|
2275
|
+
try {
|
|
2276
|
+
publishNpmWithTag(repoPath, track);
|
|
2277
|
+
const pkg = JSON.parse(readFileSync(join(repoPath, 'package.json'), 'utf8'));
|
|
2278
|
+
distResults.push({ target: 'npm', status: 'ok', detail: `${pkg.name}@${newVersion} (tag: ${track})` });
|
|
2279
|
+
console.log(` \u2713 Published to npm @${track}`);
|
|
2280
|
+
} catch (e) {
|
|
2281
|
+
distResults.push({ target: 'npm', status: 'failed', detail: e.message });
|
|
2282
|
+
console.log(` \u2717 npm publish failed: ${e.message}`);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// 6. GitHub prerelease on public repo (if opted in)
|
|
2286
|
+
if (publishReleaseNotes) {
|
|
2287
|
+
try {
|
|
2288
|
+
createGitHubPrerelease(repoPath, newVersion, notes || `${track} prerelease`);
|
|
2289
|
+
distResults.push({ target: 'GitHub', status: 'ok', detail: `v${newVersion} (prerelease)` });
|
|
2290
|
+
console.log(` \u2713 GitHub prerelease v${newVersion} created on public repo`);
|
|
2291
|
+
} catch (e) {
|
|
2292
|
+
distResults.push({ target: 'GitHub', status: 'failed', detail: e.message });
|
|
2293
|
+
console.log(` \u2717 GitHub prerelease failed: ${e.message}`);
|
|
2294
|
+
}
|
|
2295
|
+
} else {
|
|
2296
|
+
console.log(` - GitHub prerelease: skipped (silent ${track})`);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// Distribution summary
|
|
2301
|
+
if (distResults.length > 0) {
|
|
2302
|
+
console.log('');
|
|
2303
|
+
console.log(' Distribution:');
|
|
2304
|
+
for (const r of distResults) {
|
|
2305
|
+
const icon = r.status === 'ok' ? '\u2713' : '\u2717';
|
|
2306
|
+
console.log(` ${icon} ${r.target}: ${r.detail}`);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
console.log('');
|
|
2311
|
+
console.log(` Done. ${repoName} v${newVersion} (${track}) released.`);
|
|
2312
|
+
console.log('');
|
|
2313
|
+
|
|
2314
|
+
return { currentVersion, newVersion, dryRun: false };
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// ── Hotfix Track ────────────────────────────────────────────────────
|
|
2318
|
+
|
|
2319
|
+
/**
|
|
2320
|
+
* Release a hotfix.
|
|
2321
|
+
*
|
|
2322
|
+
* Same as stable patch but: no deploy-public, no code sync.
|
|
2323
|
+
* Publishes to npm @latest, creates GitHub release on public repo (opt out with publishReleaseNotes=false).
|
|
2324
|
+
*
|
|
2325
|
+
* Lighter gates than stable: no product docs check, no stale branch check.
|
|
2326
|
+
* Still runs: worktree guard, license compliance, tests.
|
|
2327
|
+
*/
|
|
2328
|
+
export async function releaseHotfix({ repoPath, notes, notesSource, dryRun, noPublish, publishReleaseNotes, skipWorktreeCheck, allowSubToolDrift }) {
|
|
2329
|
+
repoPath = repoPath || process.cwd();
|
|
2330
|
+
const currentVersion = detectCurrentVersion(repoPath);
|
|
2331
|
+
const newVersion = bumpSemver(currentVersion, 'patch');
|
|
2332
|
+
const repoName = basename(repoPath);
|
|
2333
|
+
|
|
2334
|
+
console.log('');
|
|
2335
|
+
console.log(` ${repoName}: ${currentVersion} -> ${newVersion} (hotfix)`);
|
|
2336
|
+
console.log(` ${'─'.repeat(40)}`);
|
|
2337
|
+
|
|
2338
|
+
// Main-branch guard: worktree + non-main branch check via shared helper
|
|
2339
|
+
{
|
|
2340
|
+
const guardResult = enforceMainBranchGuard(repoPath, skipWorktreeCheck);
|
|
2341
|
+
if (!guardResult.ok) {
|
|
2342
|
+
logMainBranchGuardFailure(guardResult);
|
|
2343
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2344
|
+
}
|
|
2345
|
+
if (!guardResult.skipped) {
|
|
2346
|
+
console.log(` \u2713 Running from main working tree on ${guardResult.branch ?? 'main'}`);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// License compliance gate
|
|
2351
|
+
const configPath = join(repoPath, '.license-guard.json');
|
|
2352
|
+
if (existsSync(configPath)) {
|
|
2353
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
2354
|
+
const licenseIssues = [];
|
|
2355
|
+
const licensePath = join(repoPath, 'LICENSE');
|
|
2356
|
+
if (!existsSync(licensePath)) {
|
|
2357
|
+
licenseIssues.push('LICENSE file is missing');
|
|
2358
|
+
} else {
|
|
2359
|
+
const licenseText = readFileSync(licensePath, 'utf8');
|
|
2360
|
+
if (!licenseText.includes(config.copyright)) {
|
|
2361
|
+
licenseIssues.push(`LICENSE copyright does not match "${config.copyright}"`);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
if (licenseIssues.length > 0) {
|
|
2365
|
+
console.log(` \u2717 License compliance failed:`);
|
|
2366
|
+
for (const issue of licenseIssues) console.log(` - ${issue}`);
|
|
2367
|
+
console.log('');
|
|
2368
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2369
|
+
}
|
|
2370
|
+
console.log(` \u2713 License compliance passed`);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// Release notes: hotfix accepts --notes flag as convenience (no file-only gate)
|
|
2374
|
+
if (!notes) {
|
|
2375
|
+
console.log(` ! No release notes provided. Hotfix will have minimal notes.`);
|
|
2376
|
+
notes = 'Hotfix release.';
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
// Run tests if they exist
|
|
2380
|
+
{
|
|
2381
|
+
const toolsDir = join(repoPath, 'tools');
|
|
2382
|
+
const testFiles = [];
|
|
2383
|
+
if (existsSync(toolsDir)) {
|
|
2384
|
+
for (const sub of readdirSync(toolsDir)) {
|
|
2385
|
+
const testPath = join(toolsDir, sub, 'test.sh');
|
|
2386
|
+
if (existsSync(testPath)) testFiles.push({ tool: sub, path: testPath });
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
const rootTest = join(repoPath, 'test.sh');
|
|
2390
|
+
if (existsSync(rootTest)) testFiles.push({ tool: '(root)', path: rootTest });
|
|
2391
|
+
|
|
2392
|
+
if (testFiles.length > 0) {
|
|
2393
|
+
let allPassed = true;
|
|
2394
|
+
for (const { tool, path } of testFiles) {
|
|
2395
|
+
try {
|
|
2396
|
+
execFileSync('bash', [path], { cwd: dirname(path), stdio: 'pipe', timeout: 30000 });
|
|
2397
|
+
console.log(` \u2713 Tests passed: ${tool}`);
|
|
2398
|
+
} catch (e) {
|
|
2399
|
+
allPassed = false;
|
|
2400
|
+
console.log(` \u2717 Tests FAILED: ${tool}`);
|
|
2401
|
+
const output = (e.stdout || '').toString().trim();
|
|
2402
|
+
if (output) {
|
|
2403
|
+
for (const line of output.split('\n').slice(-5)) console.log(` ${line}`);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
if (!allPassed) {
|
|
2408
|
+
console.log('');
|
|
2409
|
+
console.log(' Fix failing tests before releasing.');
|
|
2410
|
+
console.log('');
|
|
2411
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
if (dryRun) {
|
|
2417
|
+
console.log(` [dry run] Would bump package.json to ${newVersion}`);
|
|
2418
|
+
console.log(` [dry run] Would update CHANGELOG.md`);
|
|
2419
|
+
if (!noPublish) {
|
|
2420
|
+
console.log(` [dry run] Would npm publish with --tag latest`);
|
|
2421
|
+
if (publishReleaseNotes) {
|
|
2422
|
+
console.log(` [dry run] Would create GitHub release v${newVersion} on public repo`);
|
|
2423
|
+
} else {
|
|
2424
|
+
console.log(` [dry run] No GitHub release (--no-release-notes)`);
|
|
2425
|
+
}
|
|
2426
|
+
console.log(` [dry run] No deploy-public (hotfix)`);
|
|
2427
|
+
}
|
|
2428
|
+
console.log('');
|
|
2429
|
+
console.log(` Dry run complete. No changes made.`);
|
|
2430
|
+
console.log('');
|
|
2431
|
+
return { currentVersion, newVersion, dryRun: true };
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// 1.25. Pre-bump tag collision check (Phase 2).
|
|
2435
|
+
{
|
|
2436
|
+
const collision = checkTagCollision(repoPath, newVersion);
|
|
2437
|
+
if (!collision.ok) {
|
|
2438
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// 1. Bump package.json
|
|
2443
|
+
writePackageVersion(repoPath, newVersion);
|
|
2444
|
+
console.log(` \u2713 package.json -> ${newVersion}`);
|
|
2445
|
+
|
|
2446
|
+
// 1.5. Validate sub-tool version bumps (Phase 8: error by default)
|
|
2447
|
+
{
|
|
2448
|
+
const subToolResult = validateSubToolVersions(repoPath, allowSubToolDrift);
|
|
2449
|
+
if (!subToolResult.ok) {
|
|
2450
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// 2. Sync SKILL.md
|
|
2455
|
+
if (syncSkillVersion(repoPath, newVersion)) {
|
|
2456
|
+
console.log(` \u2713 SKILL.md -> ${newVersion}`);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// 3. Update CHANGELOG.md
|
|
2460
|
+
updateChangelog(repoPath, newVersion, notes);
|
|
2461
|
+
console.log(` \u2713 CHANGELOG.md updated`);
|
|
2462
|
+
|
|
2463
|
+
// 3.5. Move RELEASE-NOTES-v*.md to _trash/
|
|
2464
|
+
const trashed = trashReleaseNotes(repoPath);
|
|
2465
|
+
if (trashed > 0) {
|
|
2466
|
+
console.log(` \u2713 Moved ${trashed} RELEASE-NOTES file(s) to _trash/`);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// 4. Git commit + tag
|
|
2470
|
+
gitCommitAndTag(repoPath, newVersion, notes);
|
|
2471
|
+
console.log(` \u2713 Committed and tagged v${newVersion}`);
|
|
2472
|
+
|
|
2473
|
+
// 5. Push commit + tag
|
|
2474
|
+
try {
|
|
2475
|
+
execSync('git push && git push --tags', { cwd: repoPath, stdio: 'pipe' });
|
|
2476
|
+
console.log(` \u2713 Pushed to remote`);
|
|
2477
|
+
} catch {
|
|
2478
|
+
console.log(` ! Push failed. Push manually.`);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
const distResults = [];
|
|
2482
|
+
|
|
2483
|
+
if (!noPublish) {
|
|
2484
|
+
// 6. npm publish with @latest tag
|
|
2485
|
+
try {
|
|
2486
|
+
publishNpmWithTag(repoPath, 'latest');
|
|
2487
|
+
const pkg = JSON.parse(readFileSync(join(repoPath, 'package.json'), 'utf8'));
|
|
2488
|
+
distResults.push({ target: 'npm', status: 'ok', detail: `${pkg.name}@${newVersion}` });
|
|
2489
|
+
console.log(` \u2713 Published to npm @latest`);
|
|
2490
|
+
} catch (e) {
|
|
2491
|
+
distResults.push({ target: 'npm', status: 'failed', detail: e.message });
|
|
2492
|
+
console.log(` \u2717 npm publish failed: ${e.message}`);
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
// 7. GitHub release on public repo (not prerelease)
|
|
2496
|
+
if (publishReleaseNotes) {
|
|
2497
|
+
try {
|
|
2498
|
+
createGitHubReleaseOnPublic(repoPath, newVersion, notes, currentVersion);
|
|
2499
|
+
distResults.push({ target: 'GitHub', status: 'ok', detail: `v${newVersion}` });
|
|
2500
|
+
console.log(` \u2713 GitHub release v${newVersion} created on public repo`);
|
|
2501
|
+
} catch (e) {
|
|
2502
|
+
distResults.push({ target: 'GitHub', status: 'failed', detail: e.message });
|
|
2503
|
+
console.log(` \u2717 GitHub release failed: ${e.message}`);
|
|
2504
|
+
}
|
|
2505
|
+
} else {
|
|
2506
|
+
console.log(` - GitHub release: skipped (--no-release-notes)`);
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// No deploy-public for hotfix
|
|
2510
|
+
console.log(` - deploy-public: skipped (hotfix)`);
|
|
2511
|
+
|
|
2512
|
+
// 8. ClawHub skill publish
|
|
2513
|
+
const rootSkill = join(repoPath, 'SKILL.md');
|
|
2514
|
+
if (existsSync(rootSkill)) {
|
|
2515
|
+
try {
|
|
2516
|
+
publishClawHub(repoPath, newVersion, notes);
|
|
2517
|
+
const slug = detectSkillSlug(repoPath);
|
|
2518
|
+
distResults.push({ target: 'ClawHub', status: 'ok', detail: `${slug}@${newVersion}` });
|
|
2519
|
+
console.log(` \u2713 Published to ClawHub: ${slug}`);
|
|
2520
|
+
} catch (e) {
|
|
2521
|
+
distResults.push({ target: 'ClawHub', status: 'failed', detail: e.message });
|
|
2522
|
+
console.log(` \u2717 ClawHub publish failed: ${e.message}`);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// Distribution summary
|
|
2528
|
+
if (distResults.length > 0) {
|
|
2529
|
+
console.log('');
|
|
2530
|
+
console.log(' Distribution:');
|
|
2531
|
+
for (const r of distResults) {
|
|
2532
|
+
const icon = r.status === 'ok' ? '\u2713' : '\u2717';
|
|
2533
|
+
console.log(` ${icon} ${r.target}: ${r.detail}`);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// Write release marker
|
|
2538
|
+
try {
|
|
2539
|
+
const markerDir = join(process.env.HOME || '', '.ldm', 'state');
|
|
2540
|
+
mkdirSync(markerDir, { recursive: true });
|
|
2541
|
+
writeFileSync(join(markerDir, '.last-release'), JSON.stringify({
|
|
2542
|
+
repo: repoName,
|
|
2543
|
+
version: newVersion,
|
|
2544
|
+
timestamp: new Date().toISOString(),
|
|
2545
|
+
track: 'hotfix',
|
|
2546
|
+
}) + '\n');
|
|
2547
|
+
} catch {}
|
|
2548
|
+
|
|
2549
|
+
console.log('');
|
|
2550
|
+
console.log(` Done. ${repoName} v${newVersion} (hotfix) released.`);
|
|
2551
|
+
console.log('');
|
|
2552
|
+
|
|
2553
|
+
return { currentVersion, newVersion, dryRun: false };
|
|
2554
|
+
}
|
package/package.json
CHANGED