pi-release 1.0.0
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/LICENSE +21 -0
- package/README.md +179 -0
- package/biome.json +19 -0
- package/extensions/index.test.ts +398 -0
- package/extensions/index.ts +955 -0
- package/package.json +46 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ZachDreamZ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# pi-release
|
|
2
|
+
|
|
3
|
+
> Pi-native release automation: bump version, generate changelog, create GitHub release, publish to npm.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:pi-release
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What It Does
|
|
12
|
+
|
|
13
|
+
pi-release automates the full release lifecycle for pi.dev packages. It completes the pipeline:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pi-commit-lint → pi-changelog → pi-release
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Three focused tools and one orchestrated `/release` command handle:
|
|
20
|
+
- **Version bumping** — semver-aware `package.json` updates with git commits and tags
|
|
21
|
+
- **GitHub releases** — auto-generated release notes via GitHub CLI
|
|
22
|
+
- **npm publishing** — with OTP/2FA support and custom registries
|
|
23
|
+
|
|
24
|
+
## Tools
|
|
25
|
+
|
|
26
|
+
### `bump_version`
|
|
27
|
+
|
|
28
|
+
Bump the version in `package.json`, create a git commit and tag.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
bump_version(bump: "patch" | "minor" | "major", pre_release?: "beta", dry_run?: false)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Parameters:**
|
|
35
|
+
| Parameter | Type | Default | Description |
|
|
36
|
+
|-----------|------|---------|-------------|
|
|
37
|
+
| `bump` | `major \| minor \| patch` | `patch` | Version bump type |
|
|
38
|
+
| `pre_release` | `string` | — | Pre-release tag (e.g. `beta`, `alpha`, `rc`) |
|
|
39
|
+
| `dry_run` | `boolean` | `false` | Preview without making changes |
|
|
40
|
+
| `message` | `string` | `chore(release): {version}` | Custom commit message |
|
|
41
|
+
|
|
42
|
+
### `create_github_release`
|
|
43
|
+
|
|
44
|
+
Create a GitHub release from a git tag.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
create_github_release(tag: "v1.0.0", title?: "Release v1.0.0", notes?: "...", prerelease?: false, dry_run?: false)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Parameters:**
|
|
51
|
+
| Parameter | Type | Default | Description |
|
|
52
|
+
|-----------|------|---------|-------------|
|
|
53
|
+
| `tag` | `string` | *required* | Git tag to release from |
|
|
54
|
+
| `title` | `string` | `Release {tag}` | Release title |
|
|
55
|
+
| `notes` | `string` | *auto-generated* | Custom release notes |
|
|
56
|
+
| `prerelease` | `boolean` | `false` | Mark as pre-release |
|
|
57
|
+
| `dry_run` | `boolean` | `false` | Preview without creating |
|
|
58
|
+
|
|
59
|
+
Requires [GitHub CLI](https://cli.github.com/) (`gh`) installed and authenticated.
|
|
60
|
+
|
|
61
|
+
### `publish_npm`
|
|
62
|
+
|
|
63
|
+
Publish the package to npm.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
publish_npm(otp?: "123456", registry?: "https://...", tag?: "beta", dry_run?: false)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Parameters:**
|
|
70
|
+
| Parameter | Type | Default | Description |
|
|
71
|
+
|-----------|------|---------|-------------|
|
|
72
|
+
| `otp` | `string` | — | One-time password for npm 2FA |
|
|
73
|
+
| `registry` | `string` | `https://registry.npmjs.org/` | npm registry URL |
|
|
74
|
+
| `tag` | `string` | `latest` | npm dist-tag |
|
|
75
|
+
| `dry_run` | `boolean` | `false` | Preview without publishing |
|
|
76
|
+
|
|
77
|
+
## Command
|
|
78
|
+
|
|
79
|
+
### `/release`
|
|
80
|
+
|
|
81
|
+
Orchestrates the full release flow:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
/release [major|minor|patch] [--dry-run]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Steps:**
|
|
88
|
+
1. 📦 Bump version in `package.json`, commit, tag
|
|
89
|
+
2. 📝 Generate changelog from conventional commits
|
|
90
|
+
3. 🚀 Create GitHub release with auto-generated notes
|
|
91
|
+
4. 📤 Publish to npm
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Preview a patch release
|
|
95
|
+
/release patch --dry-run
|
|
96
|
+
|
|
97
|
+
# Execute a minor release
|
|
98
|
+
/release minor
|
|
99
|
+
|
|
100
|
+
# Major release
|
|
101
|
+
/release major
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
Create `pi-release.json` in your project root:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"registry": "https://registry.npmjs.org/",
|
|
111
|
+
"githubRepo": "owner/repo",
|
|
112
|
+
"defaultBump": "patch",
|
|
113
|
+
"preReleaseTag": "",
|
|
114
|
+
"generateChangelog": true,
|
|
115
|
+
"commitBump": true,
|
|
116
|
+
"commitMessage": "chore(release): {version}",
|
|
117
|
+
"createTag": true,
|
|
118
|
+
"tagPrefix": "v",
|
|
119
|
+
"createGithubRelease": true,
|
|
120
|
+
"publishToNpm": true
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Config options:**
|
|
125
|
+
| Option | Type | Default | Description |
|
|
126
|
+
|--------|------|---------|-------------|
|
|
127
|
+
| `registry` | `string` | `https://registry.npmjs.org/` | npm registry URL |
|
|
128
|
+
| `githubRepo` | `string \| null` | `null` (auto-detect) | GitHub `owner/repo` |
|
|
129
|
+
| `defaultBump` | `string` | `patch` | Default bump type for `/release` |
|
|
130
|
+
| `preReleaseTag` | `string` | `""` | Pre-release tag applied to all releases |
|
|
131
|
+
| `generateChangelog` | `boolean` | `true` | Auto-generate changelog |
|
|
132
|
+
| `commitBump` | `boolean` | `true` | Commit the version bump |
|
|
133
|
+
| `commitMessage` | `string` | `chore(release): {version}` | Commit message template |
|
|
134
|
+
| `createTag` | `boolean` | `true` | Create git tag |
|
|
135
|
+
| `tagPrefix` | `string` | `v` | Tag prefix (e.g. `v1.0.0`) |
|
|
136
|
+
| `createGithubRelease` | `boolean` | `true` | Create GitHub release |
|
|
137
|
+
| `publishToNpm` | `boolean` | `true` | Publish to npm |
|
|
138
|
+
|
|
139
|
+
## Dry Run
|
|
140
|
+
|
|
141
|
+
All tools support `--dry-run` mode to preview what would happen without making any changes:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Via tool
|
|
145
|
+
bump_version(bump="minor", dry_run=true)
|
|
146
|
+
|
|
147
|
+
# Via command
|
|
148
|
+
/release minor --dry-run
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Example Workflow
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# 1. Ensure commits follow conventional format
|
|
155
|
+
pi> /lint-commits
|
|
156
|
+
|
|
157
|
+
# 2. Preview the release
|
|
158
|
+
pi> /release minor --dry-run
|
|
159
|
+
|
|
160
|
+
# 3. Execute the release
|
|
161
|
+
pi> /release minor
|
|
162
|
+
|
|
163
|
+
# Output:
|
|
164
|
+
# 📦 Version Bump: 1.0.0 → 1.1.0
|
|
165
|
+
# 📝 Changelog generated
|
|
166
|
+
# 🚀 GitHub Release: https://github.com/owner/repo/releases/tag/v1.1.0
|
|
167
|
+
# 📤 Published to npm: my-package@1.1.0
|
|
168
|
+
# ✅ Release complete!
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Resources
|
|
172
|
+
|
|
173
|
+
- [npm](https://www.npmjs.com/package/pi-release)
|
|
174
|
+
- [GitHub](https://github.com/ZachDreamZ/pi-release)
|
|
175
|
+
- [pi.dev](https://pi.dev/packages/pi-release)
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
package/biome.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
|
|
3
|
+
"organizeImports": {
|
|
4
|
+
"enabled": true
|
|
5
|
+
},
|
|
6
|
+
"linter": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"rules": {
|
|
9
|
+
"recommended": true,
|
|
10
|
+
"correctness": {
|
|
11
|
+
"noUnusedVariables": "warn"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": {
|
|
16
|
+
"include": ["extensions/**/*.ts"],
|
|
17
|
+
"ignore": ["node_modules", "dist"]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
bumpSemver,
|
|
8
|
+
bumpVersionImpl,
|
|
9
|
+
createGithubReleaseImpl,
|
|
10
|
+
generateBasicChangelog,
|
|
11
|
+
parseGithubRepo,
|
|
12
|
+
publishNpmImpl,
|
|
13
|
+
} from "./index.js";
|
|
14
|
+
|
|
15
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function createTmpGitRepo(): string {
|
|
18
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "release-test-"));
|
|
19
|
+
execSync("git init", { cwd: tmpDir, stdio: "pipe" });
|
|
20
|
+
execSync("git config user.email test@test.com", {
|
|
21
|
+
cwd: tmpDir,
|
|
22
|
+
stdio: "pipe",
|
|
23
|
+
});
|
|
24
|
+
execSync("git config user.name Test", { cwd: tmpDir, stdio: "pipe" });
|
|
25
|
+
|
|
26
|
+
// Create initial package.json
|
|
27
|
+
const pkg = {
|
|
28
|
+
name: "test-package",
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
description: "Test package",
|
|
31
|
+
};
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(tmpDir, "package.json"),
|
|
34
|
+
`${JSON.stringify(pkg, null, 2)}\n`,
|
|
35
|
+
"utf-8",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
execSync("git add .", { cwd: tmpDir, stdio: "pipe" });
|
|
39
|
+
execSync('git commit -m "feat: initial commit"', {
|
|
40
|
+
cwd: tmpDir,
|
|
41
|
+
stdio: "pipe",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return tmpDir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function commit(dir: string, message: string, file = "test.txt"): void {
|
|
48
|
+
const filePath = path.join(dir, file);
|
|
49
|
+
fs.writeFileSync(filePath, `${message}\n`, "utf-8");
|
|
50
|
+
execSync(`git add ${file}`, { cwd: dir, stdio: "pipe" });
|
|
51
|
+
execSync(`git commit -m "${message}"`, { cwd: dir, stdio: "pipe" });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function tag(dir: string, tagName: string): void {
|
|
55
|
+
execSync(`git tag ${tagName}`, { cwd: dir, stdio: "pipe" });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── bumpSemver tests ────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
describe("bumpSemver", () => {
|
|
61
|
+
it("should bump patch version", () => {
|
|
62
|
+
expect(bumpSemver("1.0.0", "patch")).toBe("1.0.1");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should bump minor version", () => {
|
|
66
|
+
expect(bumpSemver("1.0.0", "minor")).toBe("1.1.0");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should bump major version", () => {
|
|
70
|
+
expect(bumpSemver("1.0.0", "major")).toBe("2.0.0");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should reset lower segments on major bump", () => {
|
|
74
|
+
expect(bumpSemver("1.2.3", "major")).toBe("2.0.0");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should reset patch on minor bump", () => {
|
|
78
|
+
expect(bumpSemver("1.2.3", "minor")).toBe("1.3.0");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should handle large version numbers", () => {
|
|
82
|
+
expect(bumpSemver("9.9.9", "patch")).toBe("9.9.10");
|
|
83
|
+
expect(bumpSemver("9.9.9", "minor")).toBe("9.10.0");
|
|
84
|
+
expect(bumpSemver("9.9.9", "major")).toBe("10.0.0");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should throw on invalid semver", () => {
|
|
88
|
+
expect(() => bumpSemver("invalid", "patch")).toThrow(
|
|
89
|
+
"Invalid semver version",
|
|
90
|
+
);
|
|
91
|
+
expect(() => bumpSemver("1.2", "patch")).toThrow("Invalid semver version");
|
|
92
|
+
expect(() => bumpSemver("1.2.x", "patch")).toThrow(
|
|
93
|
+
"Invalid semver version",
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── parseGithubRepo tests ───────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe("parseGithubRepo", () => {
|
|
101
|
+
it("should parse SSH remote", () => {
|
|
102
|
+
expect(parseGithubRepo("git@github.com:owner/repo.git")).toBe("owner/repo");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should parse HTTPS remote with .git", () => {
|
|
106
|
+
expect(parseGithubRepo("https://github.com/owner/repo.git")).toBe(
|
|
107
|
+
"owner/repo",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should parse HTTPS remote without .git", () => {
|
|
112
|
+
expect(parseGithubRepo("https://github.com/owner/repo")).toBe("owner/repo");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should return null for non-GitHub remotes", () => {
|
|
116
|
+
expect(parseGithubRepo("https://gitlab.com/owner/repo.git")).toBeNull();
|
|
117
|
+
expect(parseGithubRepo("git@gitlab.com:owner/repo.git")).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── bumpVersionImpl tests ───────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe("bumpVersionImpl", () => {
|
|
124
|
+
let tmpDir: string;
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
tmpDir = createTmpGitRepo();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should bump patch version by default", () => {
|
|
135
|
+
const result = bumpVersionImpl("patch", { cwd: tmpDir });
|
|
136
|
+
expect(result.previousVersion).toBe("1.0.0");
|
|
137
|
+
expect(result.newVersion).toBe("1.0.1");
|
|
138
|
+
expect(result.tag).toBe("v1.0.1");
|
|
139
|
+
expect(result.dryRun).toBe(false);
|
|
140
|
+
expect(result.commitHash).toBeTruthy();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should bump minor version", () => {
|
|
144
|
+
const result = bumpVersionImpl("minor", { cwd: tmpDir });
|
|
145
|
+
expect(result.newVersion).toBe("1.1.0");
|
|
146
|
+
expect(result.tag).toBe("v1.1.0");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should bump major version", () => {
|
|
150
|
+
const result = bumpVersionImpl("major", { cwd: tmpDir });
|
|
151
|
+
expect(result.newVersion).toBe("2.0.0");
|
|
152
|
+
expect(result.tag).toBe("v2.0.0");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should add pre-release tag", () => {
|
|
156
|
+
const result = bumpVersionImpl("patch", {
|
|
157
|
+
cwd: tmpDir,
|
|
158
|
+
preReleaseTag: "beta",
|
|
159
|
+
});
|
|
160
|
+
expect(result.newVersion).toBe("1.0.1-beta");
|
|
161
|
+
expect(result.tag).toBe("v1.0.1-beta");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should write new version to package.json", () => {
|
|
165
|
+
bumpVersionImpl("minor", { cwd: tmpDir });
|
|
166
|
+
const pkg = JSON.parse(
|
|
167
|
+
fs.readFileSync(path.join(tmpDir, "package.json"), "utf-8"),
|
|
168
|
+
);
|
|
169
|
+
expect(pkg.version).toBe("1.1.0");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should create git tag", () => {
|
|
173
|
+
bumpVersionImpl("patch", { cwd: tmpDir });
|
|
174
|
+
const tags = execSync("git tag", { cwd: tmpDir, encoding: "utf-8" }).trim();
|
|
175
|
+
expect(tags).toContain("v1.0.1");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should not modify files in dry run mode", () => {
|
|
179
|
+
const result = bumpVersionImpl("major", { cwd: tmpDir, dryRun: true });
|
|
180
|
+
expect(result.dryRun).toBe(true);
|
|
181
|
+
expect(result.commitHash).toBeNull();
|
|
182
|
+
|
|
183
|
+
// Verify package.json unchanged
|
|
184
|
+
const pkg = JSON.parse(
|
|
185
|
+
fs.readFileSync(path.join(tmpDir, "package.json"), "utf-8"),
|
|
186
|
+
);
|
|
187
|
+
expect(pkg.version).toBe("1.0.0");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should throw if working directory is dirty", () => {
|
|
191
|
+
fs.writeFileSync(path.join(tmpDir, "dirty.txt"), "dirty", "utf-8");
|
|
192
|
+
expect(() => bumpVersionImpl("patch", { cwd: tmpDir })).toThrow(
|
|
193
|
+
"uncommitted changes",
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should throw if tag already exists", () => {
|
|
198
|
+
tag(tmpDir, "v1.0.1");
|
|
199
|
+
expect(() => bumpVersionImpl("patch", { cwd: tmpDir })).toThrow(
|
|
200
|
+
"already exists",
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should use custom tag prefix", () => {
|
|
205
|
+
const result = bumpVersionImpl("patch", {
|
|
206
|
+
cwd: tmpDir,
|
|
207
|
+
tagPrefix: "release-",
|
|
208
|
+
});
|
|
209
|
+
expect(result.tag).toBe("release-1.0.1");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("should use custom commit message", () => {
|
|
213
|
+
const result = bumpVersionImpl("patch", {
|
|
214
|
+
cwd: tmpDir,
|
|
215
|
+
commitMessage: "release: v{version}",
|
|
216
|
+
});
|
|
217
|
+
expect(result.commitHash).toBeTruthy();
|
|
218
|
+
|
|
219
|
+
const lastMsg = execSync("git log -1 --format=%s", {
|
|
220
|
+
cwd: tmpDir,
|
|
221
|
+
encoding: "utf-8",
|
|
222
|
+
}).trim();
|
|
223
|
+
expect(lastMsg).toBe("release: v1.0.1");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ── generateBasicChangelog tests ────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
describe("generateBasicChangelog", () => {
|
|
230
|
+
let tmpDir: string;
|
|
231
|
+
|
|
232
|
+
beforeEach(() => {
|
|
233
|
+
tmpDir = createTmpGitRepo();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
afterEach(() => {
|
|
237
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should generate changelog for initial commit", () => {
|
|
241
|
+
const result = generateBasicChangelog("1.0.0", tmpDir);
|
|
242
|
+
expect(result).toContain("# Changelog");
|
|
243
|
+
expect(result).toContain("[1.0.0]");
|
|
244
|
+
expect(result).toContain("initial commit");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should group commits by type", () => {
|
|
248
|
+
commit(tmpDir, "feat: add login", "login.txt");
|
|
249
|
+
commit(tmpDir, "fix: fix crash", "fix.txt");
|
|
250
|
+
commit(tmpDir, "docs: update README", "docs.txt");
|
|
251
|
+
|
|
252
|
+
const result = generateBasicChangelog("1.1.0", tmpDir);
|
|
253
|
+
expect(result).toContain("Features");
|
|
254
|
+
expect(result).toContain("Bug Fixes");
|
|
255
|
+
expect(result).toContain("Documentation");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("should detect breaking changes", () => {
|
|
259
|
+
commit(tmpDir, "feat!: redesign API", "api.txt");
|
|
260
|
+
|
|
261
|
+
const result = generateBasicChangelog("2.0.0", tmpDir);
|
|
262
|
+
expect(result).toContain("BREAKING CHANGES");
|
|
263
|
+
expect(result).toContain("redesign API");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should handle scoped commits", () => {
|
|
267
|
+
commit(tmpDir, "feat(auth): add OAuth", "auth.txt");
|
|
268
|
+
|
|
269
|
+
const result = generateBasicChangelog("1.1.0", tmpDir);
|
|
270
|
+
expect(result).toContain("**auth:**");
|
|
271
|
+
expect(result).toContain("add OAuth");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should handle empty history gracefully", () => {
|
|
275
|
+
// Create a fresh repo with no commits beyond initial
|
|
276
|
+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), "changelog-empty-"));
|
|
277
|
+
execSync("git init", { cwd: emptyDir, stdio: "pipe" });
|
|
278
|
+
execSync("git config user.email test@test.com", {
|
|
279
|
+
cwd: emptyDir,
|
|
280
|
+
stdio: "pipe",
|
|
281
|
+
});
|
|
282
|
+
execSync("git config user.name Test", { cwd: emptyDir, stdio: "pipe" });
|
|
283
|
+
|
|
284
|
+
const result = generateBasicChangelog("0.0.1", emptyDir);
|
|
285
|
+
expect(result).toContain("# Changelog");
|
|
286
|
+
expect(result).toContain("No commits found");
|
|
287
|
+
|
|
288
|
+
fs.rmSync(emptyDir, { recursive: true, force: true });
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should limit to commits since last tag", () => {
|
|
292
|
+
commit(tmpDir, "feat: first", "a.txt");
|
|
293
|
+
tag(tmpDir, "v1.0.0");
|
|
294
|
+
commit(tmpDir, "feat: second", "b.txt");
|
|
295
|
+
commit(tmpDir, "fix: third", "c.txt");
|
|
296
|
+
|
|
297
|
+
const result = generateBasicChangelog("1.1.0", tmpDir);
|
|
298
|
+
// Should only contain commits after v1.0.0
|
|
299
|
+
expect(result).toContain("second");
|
|
300
|
+
expect(result).toContain("third");
|
|
301
|
+
expect(result).not.toContain("first");
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// ── createGithubReleaseImpl tests ───────────────────────────────────
|
|
306
|
+
|
|
307
|
+
describe("createGithubReleaseImpl", () => {
|
|
308
|
+
let tmpDir: string;
|
|
309
|
+
|
|
310
|
+
beforeEach(() => {
|
|
311
|
+
tmpDir = createTmpGitRepo();
|
|
312
|
+
// Add a GitHub remote so the function can detect the repo
|
|
313
|
+
execSync(
|
|
314
|
+
"git remote add origin https://github.com/test-owner/test-repo.git",
|
|
315
|
+
{ cwd: tmpDir, stdio: "pipe" },
|
|
316
|
+
);
|
|
317
|
+
tag(tmpDir, "v1.0.0");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
afterEach(() => {
|
|
321
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("should return dry run result without executing", () => {
|
|
325
|
+
const result = createGithubReleaseImpl("v1.0.0", {
|
|
326
|
+
cwd: tmpDir,
|
|
327
|
+
dryRun: true,
|
|
328
|
+
});
|
|
329
|
+
expect(result.dryRun).toBe(true);
|
|
330
|
+
expect(result.tag).toBe("v1.0.0");
|
|
331
|
+
expect(result.url).toContain("test-owner/test-repo");
|
|
332
|
+
expect(result.url).toContain("v1.0.0");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should throw if tag does not exist (non-dry-run)", () => {
|
|
336
|
+
expect(() =>
|
|
337
|
+
createGithubReleaseImpl("v9.9.9", { cwd: tmpDir, dryRun: false }),
|
|
338
|
+
).toThrow("does not exist");
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ── publishNpmImpl tests ────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
describe("publishNpmImpl", () => {
|
|
345
|
+
let tmpDir: string;
|
|
346
|
+
|
|
347
|
+
beforeEach(() => {
|
|
348
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "publish-test-"));
|
|
349
|
+
const pkg = {
|
|
350
|
+
name: "test-pi-release-pkg",
|
|
351
|
+
version: "1.0.0",
|
|
352
|
+
};
|
|
353
|
+
fs.writeFileSync(
|
|
354
|
+
path.join(tmpDir, "package.json"),
|
|
355
|
+
JSON.stringify(pkg, null, 2),
|
|
356
|
+
"utf-8",
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
afterEach(() => {
|
|
361
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("should return dry run result", () => {
|
|
365
|
+
const result = publishNpmImpl({ cwd: tmpDir, dryRun: true });
|
|
366
|
+
expect(result.dryRun).toBe(true);
|
|
367
|
+
expect(result.package).toBe("test-pi-release-pkg");
|
|
368
|
+
expect(result.version).toBe("1.0.0");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("should use custom registry", () => {
|
|
372
|
+
const result = publishNpmImpl({
|
|
373
|
+
cwd: tmpDir,
|
|
374
|
+
dryRun: true,
|
|
375
|
+
registry: "https://custom.registry.com/",
|
|
376
|
+
});
|
|
377
|
+
expect(result.registry).toBe("https://custom.registry.com/");
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ── Extension registration tests ────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
describe("extension registration", () => {
|
|
384
|
+
it("should export a default function", async () => {
|
|
385
|
+
const mod = await import("./index.js");
|
|
386
|
+
expect(typeof mod.default).toBe("function");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("should export helper functions directly", async () => {
|
|
390
|
+
const mod = await import("./index.js");
|
|
391
|
+
expect(typeof mod.bumpSemver).toBe("function");
|
|
392
|
+
expect(typeof mod.bumpVersionImpl).toBe("function");
|
|
393
|
+
expect(typeof mod.createGithubReleaseImpl).toBe("function");
|
|
394
|
+
expect(typeof mod.publishNpmImpl).toBe("function");
|
|
395
|
+
expect(typeof mod.generateBasicChangelog).toBe("function");
|
|
396
|
+
expect(typeof mod.parseGithubRepo).toBe("function");
|
|
397
|
+
});
|
|
398
|
+
});
|