@medyll/monorepo-pnpm-release 1.0.17
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 +172 -0
- package/bin/cli.js +26 -0
- package/package.json +51 -0
- package/src/changelog.js +23 -0
- package/src/detector.js +95 -0
- package/src/git.js +41 -0
- package/src/index.js +94 -0
- package/src/pre-publish.js +51 -0
- package/src/publisher.js +10 -0
- package/src/versioner.js +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 medyll
|
|
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,172 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
# @medyll/monorepo-pnpm-release đ¤
|
|
5
|
+
|
|
6
|
+
A lightweight, automated release manager for **pnpm workspaces**. It handles versioning, changelog generation, and publishing directly from GitHub Actions, for monorepos or standalone projects.
|
|
7
|
+
|
|
8
|
+
## ⨠Features
|
|
9
|
+
|
|
10
|
+
* **Directory-based Detection**: Only bumps packages that have actual changes in their folder.
|
|
11
|
+
* **Independent Versioning**: Each package follows its own lifecycle.
|
|
12
|
+
* **Smart Changelogs**: Injects updates into existing `CHANGELOG.md` while preserving history.
|
|
13
|
+
* **Conventional Commits**: Automatically calculates `patch`, `minor`, or `major` bumps.
|
|
14
|
+
* **Zero-Config CI**: Automatically handles Git identity (bot) if not configured.
|
|
15
|
+
* **Hybrid Support**: Works perfectly for both large monorepos and single-package projects.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## đ Installation & Usage
|
|
20
|
+
|
|
21
|
+
## CLI Options
|
|
22
|
+
|
|
23
|
+
### `--build`
|
|
24
|
+
Execute the `build` script in each changed package before releasing. If a package does not define a `build` script, it is skipped with a neutral info message.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Build changed packages only
|
|
28
|
+
npx @medyll/monorepo-pnpm-release --build
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### `--package`
|
|
32
|
+
Execute the `package` script in each changed package before releasing. If a package does not define a `package` script, it is skipped with a neutral info message.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Package changed packages only
|
|
36
|
+
npx @medyll/monorepo-pnpm-release --package
|
|
37
|
+
|
|
38
|
+
# Combine both flags
|
|
39
|
+
npx @medyll/monorepo-pnpm-release --build --package
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `--verbose`
|
|
43
|
+
Enable verbose logging with detailed output during the release process. Useful for debugging or understanding command execution.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx @medyll/monorepo-pnpm-release --build all --verbose
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `--dry-run`
|
|
50
|
+
Analyze and simulate the release without making any changes.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx @medyll/monorepo-pnpm-release --dry-run
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Option A: One-time execution (npx)
|
|
57
|
+
|
|
58
|
+
Useful to avoid polluting your dependencies.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx @medyll/monorepo-pnpm-release
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Option B: Integrated dependency
|
|
66
|
+
|
|
67
|
+
Recommended to lock the tool version for the whole team.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pnpm add -D @medyll/monorepo-pnpm-release
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then add the following script to `package.json` :
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
"scripts": {
|
|
78
|
+
"release": "monorepo-pnpm-release"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
*Usage: `pnpm release*`
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## đ Workflow Integration
|
|
88
|
+
|
|
89
|
+
Create the file `.github/workflows/release.yml` :
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
name: Release
|
|
93
|
+
on:
|
|
94
|
+
push:
|
|
95
|
+
branches: [main, develop]
|
|
96
|
+
|
|
97
|
+
jobs:
|
|
98
|
+
release:
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
permissions:
|
|
101
|
+
contents: write
|
|
102
|
+
id-token: write
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@v4
|
|
105
|
+
with:
|
|
106
|
+
fetch-depth: 0
|
|
107
|
+
|
|
108
|
+
- uses: pnpm/action-setup@v4
|
|
109
|
+
- uses: actions/setup-node@v4
|
|
110
|
+
with:
|
|
111
|
+
node-version: 20
|
|
112
|
+
registry-url: 'https://registry.npmjs.org'
|
|
113
|
+
|
|
114
|
+
- run: pnpm install --frozen-lockfile
|
|
115
|
+
- run: npx @medyll/monorepo-pnpm-release
|
|
116
|
+
env:
|
|
117
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
118
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## đ CLI Options
|
|
125
|
+
|
|
126
|
+
| Option | Alias | Description | Default |
|
|
127
|
+
| --- | --- | --- | --- |
|
|
128
|
+
| `--dry-run` | `-d` | Simulates the release without modifying Git or NPM | `false` |
|
|
129
|
+
| `--pre-id` | `-p` | Pre-release identifier (alpha, beta, next) | `alpha` |
|
|
130
|
+
| `--verbose` | `-v` | Shows detailed logs of internal steps | `false` |
|
|
131
|
+
| `--package` | `-k` | Runs `package` script in each changed package | `false` |
|
|
132
|
+
| `--build` | `-b` | Runs `build` script in each changed package | `false` |
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## đ Commit Convention
|
|
137
|
+
|
|
138
|
+
The tool analyzes your commit messages to decide the next bump:
|
|
139
|
+
|
|
140
|
+
* `fix: ...` â **patch**
|
|
141
|
+
* `feat: ...` â **minor**
|
|
142
|
+
* `feat!: ...` or `BREAKING CHANGE:` â **major**
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## đ Troubleshooting
|
|
147
|
+
|
|
148
|
+
### NPM Authentication Issues
|
|
149
|
+
|
|
150
|
+
If the CI fails at the `publish` step:
|
|
151
|
+
|
|
152
|
+
1. **Token Type**: Use an **Automation** token (not Fine-grained without write permissions).
|
|
153
|
+
2. **GitHub Secret**: Make sure the secret is named `NPM_TOKEN`.
|
|
154
|
+
3. **Access**: For `@scope/` packages, ensure they are public:
|
|
155
|
+
```json
|
|
156
|
+
"publishConfig": { "access": "public" }
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
### Git Identity Error
|
|
162
|
+
|
|
163
|
+
If you see `Author identity unknown`:
|
|
164
|
+
The tool automatically configures `github-actions[bot]`. If you use your own identity, make sure it is set before running the tool.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## đ License
|
|
169
|
+
|
|
170
|
+
MIT
|
|
171
|
+
|
|
172
|
+
---
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { executeRelease } from '../src/index.js';
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('@medyll/monorepo-pnpm-release')
|
|
10
|
+
.description('Automated release tool for pnpm workspaces and single packages')
|
|
11
|
+
.version('1.0.0')
|
|
12
|
+
.option('-d, --dry-run', 'Analyze and simulate the release without any side effects', false)
|
|
13
|
+
.option('-p, --pre-id <id>', 'Identifier for pre-release (e.g. alpha, beta, next)', 'alpha')
|
|
14
|
+
.option('-v, --verbose', 'Print detailed logs', false)
|
|
15
|
+
.option('-b, --build', 'Execute "pnpm run build" in each changed package before release', false)
|
|
16
|
+
.option('-k, --package', 'Execute "pnpm run package" in each changed package before release', false)
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
try {
|
|
19
|
+
await executeRelease(options);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error(`\nâ Execution failed: ${error.message}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@medyll/monorepo-pnpm-release",
|
|
3
|
+
"version": "1.0.17",
|
|
4
|
+
"description": "Minimalist monorepo release tool for pnpm workspaces",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"module": "src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"monorepo-pnpm-release": "./bin/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"bin",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"pnpm",
|
|
21
|
+
"monorepo",
|
|
22
|
+
"release",
|
|
23
|
+
"versioning",
|
|
24
|
+
"changelog"
|
|
25
|
+
],
|
|
26
|
+
"author": "Mydde",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"packageConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"name": "@medyll/monorepo-pnpm-release",
|
|
31
|
+
"directory": "."
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@pnpm/find-workspace-packages": "^6.0.9",
|
|
35
|
+
"commander": "^14.0.3",
|
|
36
|
+
"conventional-commits-parser": "^6.2.1",
|
|
37
|
+
"execa": "^9.6.1",
|
|
38
|
+
"semver": "^7.7.3"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.1.0",
|
|
42
|
+
"@types/semver": "^7.7.1",
|
|
43
|
+
"typescript": "^5.9.3"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"start": "node bin/cli.js",
|
|
47
|
+
"release": "node bin/cli.js",
|
|
48
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
49
|
+
"check-types": "tsc --noEmit"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/changelog.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// author : Lebrun Meddy
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export async function updateChangelog(pkg, { verbose } = {}) {
|
|
6
|
+
const file = path.join(pkg.dir, 'CHANGELOG.md');
|
|
7
|
+
let content = '';
|
|
8
|
+
try {
|
|
9
|
+
content = await fs.readFile(file, 'utf-8');
|
|
10
|
+
if (verbose) console.log(`[verbose] Read existing changelog for ${pkg.name}`);
|
|
11
|
+
} catch {
|
|
12
|
+
content = '# Changelog\n';
|
|
13
|
+
if (verbose) console.log(`[verbose] No existing changelog for ${pkg.name}, creating new.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const newEntry = `## [${pkg.newVersion}] - ${new Date().toISOString().split('T')[0]}\n- Update dependencies and fixes.\n`;
|
|
17
|
+
if (verbose) console.log(`[verbose] Adding changelog entry for ${pkg.name}:`, newEntry);
|
|
18
|
+
// Insert after the first H1
|
|
19
|
+
const lines = content.split('\n');
|
|
20
|
+
lines.splice(1, 0, '\n' + newEntry);
|
|
21
|
+
await fs.writeFile(file, lines.join('\n'));
|
|
22
|
+
if (verbose) console.log(`[verbose] Wrote changelog for ${pkg.name}`);
|
|
23
|
+
}
|
package/src/detector.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// author : Lebrun Meddy
|
|
2
|
+
import findWorkspacePackages from '@pnpm/find-workspace-packages';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the most recent git tag for a specific package
|
|
9
|
+
*/
|
|
10
|
+
async function getLastTag(packageName) {
|
|
11
|
+
try {
|
|
12
|
+
const { stdout } = await execa('git', [
|
|
13
|
+
'describe',
|
|
14
|
+
'--tags',
|
|
15
|
+
'--match', `${packageName}@*`,
|
|
16
|
+
'--abbrev=0'
|
|
17
|
+
]);
|
|
18
|
+
return stdout;
|
|
19
|
+
} catch {
|
|
20
|
+
// Fallback for single packages or missing tags
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Identify which packages need a release
|
|
27
|
+
* Works for both pnpm workspaces and single packages
|
|
28
|
+
*/
|
|
29
|
+
export async function analyzeChanges({ verbose } = {}) {
|
|
30
|
+
let allPackages = [];
|
|
31
|
+
if (verbose) console.log('[verbose] Detecting workspace packages...');
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Robust import handling for pnpm's internal tool
|
|
35
|
+
const getPkgs = typeof findWorkspacePackages === 'function'
|
|
36
|
+
? findWorkspacePackages
|
|
37
|
+
: findWorkspacePackages.default;
|
|
38
|
+
|
|
39
|
+
allPackages = await getPkgs('.')
|
|
40
|
+
if (verbose) console.log('[verbose] Found packages:', allPackages.map(p => p.manifest?.name || p.dir));
|
|
41
|
+
// If workspace detection returns nothing, force fallback
|
|
42
|
+
if (!allPackages || allPackages.length === 0) {
|
|
43
|
+
throw new Error('No workspace found');
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// Fallback: Check if the current directory is a standard pnpm package
|
|
47
|
+
const manifestPath = path.join(process.cwd(), 'package.json');
|
|
48
|
+
try {
|
|
49
|
+
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
50
|
+
const manifest = JSON.parse(content);
|
|
51
|
+
|
|
52
|
+
// We wrap the single package in the same structure as a workspace result
|
|
53
|
+
allPackages = [{
|
|
54
|
+
dir: process.cwd(),
|
|
55
|
+
manifest: manifest
|
|
56
|
+
}];
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (verbose) console.log('[verbose] No pnpm workspace or package.json found in this directory.');
|
|
59
|
+
console.error("â No pnpm workspace or package.json found in this directory.");
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const packagesToRelease = [];
|
|
65
|
+
|
|
66
|
+
for (const pkg of allPackages) {
|
|
67
|
+
if (verbose) console.log(`[verbose] Checking package: ${pkg.manifest.name}`);
|
|
68
|
+
// Skip private packages unless they are the root fallback
|
|
69
|
+
if (pkg.manifest.private && allPackages.length > 1) continue;
|
|
70
|
+
|
|
71
|
+
const lastTag = await getLastTag(pkg.manifest.name);
|
|
72
|
+
const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
|
|
73
|
+
if (verbose) console.log(`[verbose] Using git range: ${range}`);
|
|
74
|
+
|
|
75
|
+
// Get logs for the package directory
|
|
76
|
+
// pkg.dir is absolute path, works for both workspace and root
|
|
77
|
+
const { stdout } = await execa('git', ['log', range, '--format=%B', '--', pkg.dir]);
|
|
78
|
+
if (verbose) console.log(`[verbose] Git log for ${pkg.manifest.name}:`, stdout);
|
|
79
|
+
|
|
80
|
+
// Split commits by double newline to separate them properly
|
|
81
|
+
const commits = stdout.split('\n').filter(Boolean);
|
|
82
|
+
|
|
83
|
+
if (commits.length > 0) {
|
|
84
|
+
packagesToRelease.push({
|
|
85
|
+
name: pkg.manifest.name,
|
|
86
|
+
dir: pkg.dir,
|
|
87
|
+
currentVersion: pkg.manifest.version,
|
|
88
|
+
rawCommits: commits
|
|
89
|
+
});
|
|
90
|
+
if (verbose) console.log(`[verbose] Package ${pkg.manifest.name} scheduled for release.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return packagesToRelease;
|
|
95
|
+
}
|
package/src/git.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Commits & Tags multiples
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
// author : Lebrun Meddy
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if git user is configured, otherwise set a default bot identity
|
|
7
|
+
*/
|
|
8
|
+
async function ensureGitIdentity(verbose) {
|
|
9
|
+
try {
|
|
10
|
+
await execa('git', ['config', 'user.name']);
|
|
11
|
+
if (verbose) console.log('[verbose] Git user.name is set.');
|
|
12
|
+
} catch {
|
|
13
|
+
console.log("đ§ No git identity found. Setting default bot identity...");
|
|
14
|
+
if (verbose) console.log('[verbose] Would set git user.name and user.email to github-actions[bot]');
|
|
15
|
+
/* await execa('git', ['config', 'user.name', 'github-actions[bot]']);
|
|
16
|
+
await execa('git', ['config', 'user.email', 'github-actions[bot]@users.noreply.github.com']); */
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function finalizeGit(releasedPackages, { verbose } = {}) {
|
|
21
|
+
// Check and set identity if needed
|
|
22
|
+
await ensureGitIdentity(verbose);
|
|
23
|
+
if (verbose) console.log('[verbose] Staging all changes for commit.');
|
|
24
|
+
await execa('git', ['add', '.']);
|
|
25
|
+
|
|
26
|
+
const commitMessage = `chore(release): publish packages\n\n${releasedPackages
|
|
27
|
+
.map(p => `- ${p.name}@${p.newVersion}`)
|
|
28
|
+
.join('\n')}`;
|
|
29
|
+
if (verbose) console.log('[verbose] Commit message:', commitMessage);
|
|
30
|
+
await execa('git', ['commit', '-m', commitMessage]);
|
|
31
|
+
|
|
32
|
+
for (const pkg of releasedPackages) {
|
|
33
|
+
const tagName = `${pkg.name}@${pkg.newVersion}`;
|
|
34
|
+
if (verbose) console.log(`[verbose] Tagging ${tagName}`);
|
|
35
|
+
// -f (force) prevents crashing if the tag was locally created before
|
|
36
|
+
await execa('git', ['tag', '-a', tagName, '-m', tagName, '-f']);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (verbose) console.log('[verbose] Pushing tags and commits to origin.');
|
|
40
|
+
await execa('git', ['push', 'origin', '--follow-tags']);
|
|
41
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// author : Lebrun Meddy
|
|
2
|
+
import { analyzeChanges } from "./detector.js";
|
|
3
|
+
import { bumpPackages } from "./versioner.js";
|
|
4
|
+
import { updateChangelog } from "./changelog.js";
|
|
5
|
+
import { finalizeGit } from "./git.js";
|
|
6
|
+
import { publishToRegistry } from "./publisher.js";
|
|
7
|
+
import { executePrePublishCommands } from "./pre-publish.js";
|
|
8
|
+
|
|
9
|
+
// Simple verbose logger
|
|
10
|
+
function vLog(verbose, ...args) {
|
|
11
|
+
if (verbose) {
|
|
12
|
+
console.log(`${"\x1b[90m"}[verbose]`, ...args, "\x1b[0m");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ANSI Color codes
|
|
17
|
+
const colors = {
|
|
18
|
+
reset: "\x1b[0m",
|
|
19
|
+
bright: "\x1b[1m",
|
|
20
|
+
green: "\x1b[32m",
|
|
21
|
+
yellow: "\x1b[33m",
|
|
22
|
+
blue: "\x1b[34m",
|
|
23
|
+
magenta: "\x1b[35m",
|
|
24
|
+
cyan: "\x1b[36m",
|
|
25
|
+
red: "\x1b[31m",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function executeRelease(options) {
|
|
29
|
+
const isPre = process.env.GITHUB_REF !== "refs/heads/main";
|
|
30
|
+
const mode = isPre ? `PRE-RELEASE (${options.preId})` : "STABLE";
|
|
31
|
+
const verbose = options.verbose;
|
|
32
|
+
|
|
33
|
+
console.log(
|
|
34
|
+
`\n${colors.bright}${colors.blue}đ Starting release process in ${mode} mode...${colors.reset}\n`,
|
|
35
|
+
);
|
|
36
|
+
vLog(verbose, "Options:", options);
|
|
37
|
+
|
|
38
|
+
// 1. Detection
|
|
39
|
+
console.log(`${colors.cyan}đ Analyzing changes...${colors.reset}`);
|
|
40
|
+
const changes = await analyzeChanges({ verbose });
|
|
41
|
+
|
|
42
|
+
if (!changes.length) {
|
|
43
|
+
console.log(
|
|
44
|
+
`${colors.yellow}⨠Nothing to release. All packages are up to date.${colors.reset}`,
|
|
45
|
+
);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(
|
|
50
|
+
`${colors.green}Found ${changes.length} package(s) with changes.${colors.reset}`,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// 2. Pre-publish commands (per package)
|
|
54
|
+
await executePrePublishCommands(changes, options);
|
|
55
|
+
|
|
56
|
+
// 3. Bumping versions
|
|
57
|
+
console.log(`${colors.cyan}đ Bumping versions...${colors.reset}`);
|
|
58
|
+
const released = await bumpPackages(changes, isPre, options.preId, { verbose });
|
|
59
|
+
|
|
60
|
+
for (const pkg of released) {
|
|
61
|
+
console.log(
|
|
62
|
+
` - ${colors.bright}${pkg.name}${colors.reset}: ${colors.yellow}${pkg.currentVersion}${colors.reset} -> ${colors.green}${pkg.newVersion}${colors.reset}`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// 3. Changelogs
|
|
66
|
+
console.log(` - ${colors.blue}đ Updating CHANGELOG.md...${colors.reset}`);
|
|
67
|
+
await updateChangelog(pkg, { verbose });
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 5. Execution or Dry Run
|
|
72
|
+
if (!options.dryRun) {
|
|
73
|
+
console.log(
|
|
74
|
+
`\n${colors.magenta}đ Finalizing Git operations (commit & tags)...${colors.reset}`,
|
|
75
|
+
);
|
|
76
|
+
await finalizeGit(released, { verbose });
|
|
77
|
+
|
|
78
|
+
console.log(`${colors.magenta}đĻ Publishing to registry...${colors.reset}`);
|
|
79
|
+
await publishToRegistry(released, isPre ? options.preId : "latest", {
|
|
80
|
+
verbose,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
console.log(
|
|
84
|
+
`\n${colors.bright}${colors.green}đ Release successfully finished!${colors.reset}\n`,
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
console.log(
|
|
88
|
+
`\n${colors.bright}${colors.yellow}â ī¸ DRY RUN COMPLETED${colors.reset}`,
|
|
89
|
+
);
|
|
90
|
+
console.log(
|
|
91
|
+
`${colors.yellow}No changes were pushed to Git or NPM.${colors.reset}\n`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
async function readPackageManifest(pkgDir) {
|
|
6
|
+
const manifestPath = path.join(pkgDir, "package.json");
|
|
7
|
+
const content = await fs.readFile(manifestPath, "utf-8");
|
|
8
|
+
return JSON.parse(content);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function runPackageScript(pkgDir, scriptName) {
|
|
12
|
+
console.log(` âī¸ pnpm run ${scriptName}...`);
|
|
13
|
+
await execa("pnpm", ["run", scriptName], { cwd: pkgDir });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Execute pre-publish commands (build and/or package) before release
|
|
18
|
+
* @param {Array} packages - Packages to process
|
|
19
|
+
* @param {Object} options - CLI options
|
|
20
|
+
* @param {boolean} options.build - Whether to run build
|
|
21
|
+
* @param {boolean} options.package - Whether to run package
|
|
22
|
+
*/
|
|
23
|
+
export async function executePrePublishCommands(packages, options) {
|
|
24
|
+
const requestedScripts = [];
|
|
25
|
+
if (options.build) requestedScripts.push("build");
|
|
26
|
+
if (options.package) requestedScripts.push("package");
|
|
27
|
+
|
|
28
|
+
if (requestedScripts.length === 0) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const pkg of packages) {
|
|
33
|
+
const manifest = await readPackageManifest(pkg.dir);
|
|
34
|
+
const scripts = manifest.scripts || {};
|
|
35
|
+
const missingScripts = requestedScripts.filter(
|
|
36
|
+
(scriptName) => !scripts[scriptName],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (missingScripts.length > 0) {
|
|
40
|
+
console.log(
|
|
41
|
+
` âšī¸ ${manifest.name}: missing script(s): ${missingScripts.join(", ")}. Skipping.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const scriptName of requestedScripts) {
|
|
46
|
+
if (scripts[scriptName]) {
|
|
47
|
+
await runPackageScript(pkg.dir, scriptName);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/publisher.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// author : Lebrun Meddy
|
|
2
|
+
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
|
|
5
|
+
export async function publishToRegistry(released, tag, { verbose } = {}) {
|
|
6
|
+
for (const pkg of released) {
|
|
7
|
+
if (verbose) console.log(`[verbose] Publishing ${pkg.name} with tag ${tag}`);
|
|
8
|
+
await execa('pnpm', ['publish', '--filter', pkg.name, '--tag', tag, '--no-git-checks','--access', 'public'], { stdio: 'inherit' });
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/versioner.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// author : Lebrun Meddy
|
|
2
|
+
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
export async function bumpPackages(packages, isPre, preId, { verbose } = {}) {
|
|
8
|
+
return Promise.all(packages.map(async (pkg) => {
|
|
9
|
+
if (verbose) console.log(`[verbose] Bumping version for ${pkg.name}`);
|
|
10
|
+
// Logic: determine bump type (feat=minor, fix=patch, etc.)
|
|
11
|
+
// For this example, we assume 'patch' or 'prepatch'
|
|
12
|
+
const type = isPre ? 'prepatch' : 'patch';
|
|
13
|
+
const next = semver.inc(pkg.currentVersion, type, preId);
|
|
14
|
+
if (verbose) console.log(`[verbose] New version for ${pkg.name}: ${next}`);
|
|
15
|
+
|
|
16
|
+
const pJsonPath = path.join(pkg.dir, 'package.json');
|
|
17
|
+
const manifest = JSON.parse(await fs.readFile(pJsonPath, 'utf-8'));
|
|
18
|
+
manifest.version = next;
|
|
19
|
+
if (verbose) console.log(`[verbose] Writing new version to ${pJsonPath}`);
|
|
20
|
+
await fs.writeFile(pJsonPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
21
|
+
return { ...pkg, newVersion: next };
|
|
22
|
+
}));
|
|
23
|
+
}
|