@oorabona/release-it-preset 0.8.1 → 0.10.1
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/README.md +130 -2
- package/bin/cli.js +122 -9
- package/bin/validators.js +64 -12
- package/config/constants.js +8 -3
- package/dist/scripts/check-config.js +2 -5
- package/dist/scripts/check-pr-status.js +8 -5
- package/dist/scripts/extract-changelog.js +9 -11
- package/dist/scripts/init-project.js +21 -21
- package/dist/scripts/lib/errors.js +45 -0
- package/dist/scripts/lib/run-script.js +28 -0
- package/dist/scripts/populate-unreleased-changelog.js +18 -16
- package/dist/scripts/republish-changelog.js +15 -21
- package/dist/scripts/retry-publish.js +3 -6
- package/dist/scripts/validate-release.js +4 -8
- package/dist/types/lib/errors.d.ts +44 -0
- package/dist/types/lib/run-script.d.ts +15 -0
- package/package.json +12 -10
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@ Shared [release-it](https://github.com/release-it/release-it) configuration and
|
|
|
4
4
|
|
|
5
5
|
[](https://codecov.io/github/oorabona/release-it-preset)
|
|
6
6
|
[](https://github.com/oorabona/release-it-preset/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/oorabona/release-it-preset/actions/workflows/audit.yml)
|
|
7
8
|
[](https://npmjs.org/package/@oorabona/release-it-preset)
|
|
8
9
|
[](https://npmjs.org/package/@oorabona/release-it-preset)
|
|
9
10
|
[](https://www.typescriptlang.org/)
|
|
@@ -16,6 +17,11 @@ Shared [release-it](https://github.com/release-it/release-it) configuration and
|
|
|
16
17
|
- [Quick Start](#quick-start)
|
|
17
18
|
- [Available Configurations](#available-configurations)
|
|
18
19
|
- [CLI Usage](#cli-usage)
|
|
20
|
+
- [Zero-Config Mode (Auto-Detection)](#zero-config-mode-auto-detection)
|
|
21
|
+
- [Preset Selection Mode](#preset-selection-mode)
|
|
22
|
+
- [Passthrough Mode (Custom Config Override)](#passthrough-mode-custom-config-override)
|
|
23
|
+
- [Monorepo Support](#monorepo-support)
|
|
24
|
+
- [Utility Commands](#utility-commands)
|
|
19
25
|
- [Scripts](#scripts)
|
|
20
26
|
- [Environment Variables](#environment-variables)
|
|
21
27
|
- [Configuration Override](#configuration-override)
|
|
@@ -37,6 +43,9 @@ Shared [release-it](https://github.com/release-it/release-it) configuration and
|
|
|
37
43
|
- 🔄 Republish and retry mechanisms for failed releases
|
|
38
44
|
- ⚡ Hotfix release support
|
|
39
45
|
- 🎯 Environment variable configuration
|
|
46
|
+
- 🔍 **NEW v0.9.0:** Zero-config auto-detection mode
|
|
47
|
+
- 🏢 **NEW v0.9.0:** Monorepo support with parent directory config references
|
|
48
|
+
- ⚙️ **NEW v0.9.0:** Passthrough mode for custom config files
|
|
40
49
|
|
|
41
50
|
## Installation
|
|
42
51
|
|
|
@@ -331,9 +340,42 @@ Features:
|
|
|
331
340
|
|
|
332
341
|
## CLI Usage
|
|
333
342
|
|
|
334
|
-
The package provides a `release-it-preset` CLI with
|
|
343
|
+
The package provides a `release-it-preset` CLI with four operating modes:
|
|
335
344
|
|
|
336
|
-
|
|
345
|
+
1. **Zero-Config Mode** (auto-detection) - No arguments needed
|
|
346
|
+
2. **Preset Selection Mode** - Specify which preset to use
|
|
347
|
+
3. **Passthrough Mode** - Direct config file override
|
|
348
|
+
4. **Utility Mode** - Helper commands
|
|
349
|
+
|
|
350
|
+
### Zero-Config Mode (Auto-Detection)
|
|
351
|
+
|
|
352
|
+
**NEW in v0.9.0** - The CLI can automatically detect which preset to use from your `.release-it.json`:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# Just run release-it-preset with no arguments
|
|
356
|
+
pnpm release-it-preset
|
|
357
|
+
|
|
358
|
+
# 🔍 Auto-detected preset: default
|
|
359
|
+
# ✅ Config validated: preset "default"
|
|
360
|
+
# 📝 Using: /path/to/.release-it.json
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**How it works:**
|
|
364
|
+
1. CLI reads your `.release-it.json`
|
|
365
|
+
2. Extracts the preset name from the `extends` field
|
|
366
|
+
3. Runs that preset automatically
|
|
367
|
+
|
|
368
|
+
**Requirements:**
|
|
369
|
+
- `.release-it.json` must exist
|
|
370
|
+
- Must have `extends` field like `"@oorabona/release-it-preset/config/default"`
|
|
371
|
+
|
|
372
|
+
**Benefits:**
|
|
373
|
+
- ✅ Shortest command possible
|
|
374
|
+
- ✅ Config file is source of truth
|
|
375
|
+
- ✅ No need to remember preset names
|
|
376
|
+
- ✅ Follows industry standards (ESLint, TypeScript, Prettier)
|
|
377
|
+
|
|
378
|
+
### Preset Selection Mode
|
|
337
379
|
|
|
338
380
|
Run release-it with specific configurations:
|
|
339
381
|
|
|
@@ -350,6 +392,92 @@ pnpm release-it-preset manual-changelog
|
|
|
350
392
|
|
|
351
393
|
All additional arguments are passed through to release-it.
|
|
352
394
|
|
|
395
|
+
### Passthrough Mode (Custom Config Override)
|
|
396
|
+
|
|
397
|
+
**NEW in v0.9.0** - Use a custom config file and bypass preset validation:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
# Use custom config file
|
|
401
|
+
pnpm release-it-preset --config .release-it-manual.json
|
|
402
|
+
|
|
403
|
+
# 🔀 Passthrough mode: using config .release-it-manual.json
|
|
404
|
+
# Bypassing preset validation - direct release-it invocation
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Use cases:**
|
|
408
|
+
- **Switching presets occasionally** - Have multiple config files for different scenarios
|
|
409
|
+
- **Monorepo workflows** - Reference shared configs from parent directories
|
|
410
|
+
- **Advanced customization** - Full control over release-it configuration
|
|
411
|
+
|
|
412
|
+
**Example workflow:**
|
|
413
|
+
|
|
414
|
+
```json
|
|
415
|
+
// .release-it.json (default - 95% of time)
|
|
416
|
+
{
|
|
417
|
+
"extends": "@oorabona/release-it-preset/config/default",
|
|
418
|
+
"git": { "requireBranch": "develop" }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// .release-it-manual.json (rare - 5% of time)
|
|
422
|
+
{
|
|
423
|
+
"extends": "@oorabona/release-it-preset/config/manual-changelog",
|
|
424
|
+
"git": { "requireBranch": "develop" }
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
# Normal release
|
|
430
|
+
pnpm release-it-preset # Auto-detects default
|
|
431
|
+
|
|
432
|
+
# Manual changelog release (rare)
|
|
433
|
+
pnpm release-it-preset --config .release-it-manual.json
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Benefits:**
|
|
437
|
+
- ✅ No need to edit `.release-it.json` to switch presets
|
|
438
|
+
- ✅ Config files are explicit and version-controlled
|
|
439
|
+
- ✅ Works with monorepo parent directory references
|
|
440
|
+
|
|
441
|
+
### Monorepo Support
|
|
442
|
+
|
|
443
|
+
**NEW in v0.9.0** - Parent directory config references are now supported:
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
# Monorepo structure
|
|
447
|
+
/my-monorepo/
|
|
448
|
+
├── .release-it-base.json # Shared configuration
|
|
449
|
+
├── packages/
|
|
450
|
+
│ ├── core/
|
|
451
|
+
│ │ └── .release-it.json # extends: ../../.release-it-base.json
|
|
452
|
+
│ └── utils/
|
|
453
|
+
│ └── .release-it.json # extends: ../../.release-it-base.json
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
```json
|
|
457
|
+
// packages/core/.release-it.json
|
|
458
|
+
{
|
|
459
|
+
"extends": [
|
|
460
|
+
"../../.release-it-base.json", // ✅ Parent reference allowed!
|
|
461
|
+
"@oorabona/release-it-preset/config/default"
|
|
462
|
+
]
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Security validation:**
|
|
467
|
+
- ✅ Parent directory references (`../`) supported (up to 5 levels)
|
|
468
|
+
- ✅ Config file extension whitelist (`.json`, `.js`, `.cjs`, `.mjs`, `.yaml`, `.yml`, `.toml`)
|
|
469
|
+
- ✅ File existence validation
|
|
470
|
+
- ❌ Absolute paths blocked (use relative paths)
|
|
471
|
+
- ❌ Excessive traversal blocked (max `../../../../../../`)
|
|
472
|
+
|
|
473
|
+
**Why this is safe:**
|
|
474
|
+
- Config files are trusted code (developer controls the repository)
|
|
475
|
+
- Industry standard pattern (TypeScript, ESLint, Prettier all allow `../`)
|
|
476
|
+
- Multiple validation layers prevent abuse
|
|
477
|
+
- No privilege escalation in CLI tool context
|
|
478
|
+
|
|
479
|
+
See [examples/monorepo-workflow.md](examples/monorepo-workflow.md) for complete monorepo guide.
|
|
480
|
+
|
|
353
481
|
### Utility Commands
|
|
354
482
|
|
|
355
483
|
Helper commands for project setup and maintenance:
|
package/bin/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ import { spawn } from 'node:child_process';
|
|
|
27
27
|
import { existsSync, readFileSync } from 'node:fs';
|
|
28
28
|
import { fileURLToPath } from 'node:url';
|
|
29
29
|
import { dirname, join } from 'node:path';
|
|
30
|
-
import { validateConfigName, validateUtilityCommand, sanitizeArgs } from './validators.js';
|
|
30
|
+
import { validateConfigName, validateUtilityCommand, sanitizeArgs, validateConfigPath } from './validators.js';
|
|
31
31
|
|
|
32
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
33
33
|
const __dirname = dirname(__filename);
|
|
@@ -54,7 +54,14 @@ const UTILITY_COMMANDS = {
|
|
|
54
54
|
|
|
55
55
|
function showHelp() {
|
|
56
56
|
console.log(`
|
|
57
|
-
Usage: release-it-preset
|
|
57
|
+
Usage: release-it-preset [command] [...args]
|
|
58
|
+
release-it-preset --config <file> [...args]
|
|
59
|
+
|
|
60
|
+
CLI Modes:
|
|
61
|
+
1. Auto-detection (no command) - Reads preset from .release-it.json
|
|
62
|
+
2. Preset selection - Specify preset command
|
|
63
|
+
3. Passthrough (--config) - Direct config file, bypass validation
|
|
64
|
+
4. Utility commands - Helper scripts
|
|
58
65
|
|
|
59
66
|
Release Commands:
|
|
60
67
|
default Full release with changelog, git, GitHub, and npm
|
|
@@ -73,19 +80,28 @@ Utility Commands:
|
|
|
73
80
|
check-pr Evaluate PR hygiene (branch diff, changelog status, conventions)
|
|
74
81
|
retry-publish-preflight Run retry publish safety checks without executing release
|
|
75
82
|
|
|
83
|
+
Passthrough Mode:
|
|
84
|
+
--config <file> Use custom config file, bypass preset validation
|
|
85
|
+
|
|
76
86
|
Examples:
|
|
87
|
+
# Zero-config (auto-detect from .release-it.json)
|
|
88
|
+
release-it-preset
|
|
89
|
+
|
|
77
90
|
# Release commands
|
|
78
91
|
release-it-preset default --dry-run
|
|
79
92
|
release-it-preset hotfix --verbose
|
|
80
93
|
release-it-preset changelog-only --ci
|
|
81
94
|
|
|
95
|
+
# Passthrough mode (custom config)
|
|
96
|
+
release-it-preset --config .release-it-manual.json
|
|
97
|
+
|
|
98
|
+
# Monorepo (parent config reference)
|
|
99
|
+
release-it-preset --config ../../.release-it-base.json
|
|
100
|
+
|
|
82
101
|
# Utility commands
|
|
83
102
|
release-it-preset init
|
|
84
103
|
release-it-preset update
|
|
85
104
|
release-it-preset validate
|
|
86
|
-
release-it-preset check
|
|
87
|
-
release-it-preset check-pr
|
|
88
|
-
release-it-preset retry-publish-preflight
|
|
89
105
|
|
|
90
106
|
For release-it options, see: https://github.com/release-it/release-it
|
|
91
107
|
For environment variables, see: https://github.com/oorabona/release-it-preset#environment-variables
|
|
@@ -138,7 +154,7 @@ function handleReleaseCommand(configName, args) {
|
|
|
138
154
|
}
|
|
139
155
|
|
|
140
156
|
// Validate extends matches CLI preset
|
|
141
|
-
const extendsMatch = userConfig.extends.match(/@oorabona\/release-it-preset\/config\/(\w+)/);
|
|
157
|
+
const extendsMatch = userConfig.extends.match(/@oorabona\/release-it-preset\/config\/([\w-]+)/);
|
|
142
158
|
const extendsPreset = extendsMatch?.[1];
|
|
143
159
|
|
|
144
160
|
if (extendsPreset && extendsPreset !== configName) {
|
|
@@ -237,10 +253,53 @@ function handleUtilityCommand(commandName, args) {
|
|
|
237
253
|
});
|
|
238
254
|
}
|
|
239
255
|
|
|
256
|
+
function passthroughToReleaseIt(args) {
|
|
257
|
+
// Extract --config value
|
|
258
|
+
const configIndex = args.indexOf('--config');
|
|
259
|
+
if (configIndex === -1 || configIndex === args.length - 1) {
|
|
260
|
+
console.error('❌ --config requires a file path argument\n');
|
|
261
|
+
console.error('Usage: release-it-preset --config <file>');
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const configPath = args[configIndex + 1];
|
|
266
|
+
|
|
267
|
+
// Security validation
|
|
268
|
+
try {
|
|
269
|
+
validateConfigPath(configPath);
|
|
270
|
+
sanitizeArgs(args);
|
|
271
|
+
|
|
272
|
+
console.log(`🔀 Passthrough mode: using config ${configPath}`);
|
|
273
|
+
console.log(` Bypassing preset validation - direct release-it invocation\n`);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.error(`❌ Configuration validation failed: ${error.message}`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Delegate to release-it
|
|
280
|
+
const releaseItCommand = 'release-it';
|
|
281
|
+
const child = spawn(releaseItCommand, args, {
|
|
282
|
+
stdio: 'inherit',
|
|
283
|
+
shell: false, // Security: prevent command injection
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
child.on('error', (error) => {
|
|
287
|
+
console.error(`❌ Failed to start release-it: ${error.message}`);
|
|
288
|
+
console.error(`\nMake sure release-it is installed:`);
|
|
289
|
+
console.error(` pnpm add -D release-it`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
child.on('close', (code) => {
|
|
294
|
+
process.exit(code ?? 0);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
240
298
|
function main() {
|
|
241
299
|
const args = process.argv.slice(2);
|
|
242
300
|
|
|
243
|
-
|
|
301
|
+
// Handle --help
|
|
302
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
244
303
|
showHelp();
|
|
245
304
|
process.exit(0);
|
|
246
305
|
}
|
|
@@ -248,13 +307,66 @@ function main() {
|
|
|
248
307
|
const command = args[0];
|
|
249
308
|
const commandArgs = args.slice(1);
|
|
250
309
|
|
|
251
|
-
// Check
|
|
310
|
+
// Check for conflicting arguments (preset command + --config)
|
|
311
|
+
if (args.includes('--config') && RELEASE_CONFIGS[command]) {
|
|
312
|
+
console.error('❌ Conflicting arguments detected!\n');
|
|
313
|
+
console.error(' You specified both a preset command and --config flag.');
|
|
314
|
+
console.error('');
|
|
315
|
+
console.error(' Either:');
|
|
316
|
+
console.error(` 1. Use preset: release-it-preset ${command}`);
|
|
317
|
+
console.error(` 2. Use config: release-it-preset --config <file>`);
|
|
318
|
+
console.error('');
|
|
319
|
+
console.error(' Do not mix both approaches.');
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// MODE 1: Passthrough - Direct release-it with custom config
|
|
324
|
+
if (args.includes('--config')) {
|
|
325
|
+
passthroughToReleaseIt(args);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// MODE 2: Auto-detection - No arguments, read preset from .release-it.json
|
|
330
|
+
if (args.length === 0) {
|
|
331
|
+
const userConfigPath = join(process.cwd(), '.release-it.json');
|
|
332
|
+
|
|
333
|
+
if (!existsSync(userConfigPath)) {
|
|
334
|
+
console.error('❌ No command specified and no .release-it.json found\n');
|
|
335
|
+
console.error('Either:');
|
|
336
|
+
console.error(' 1. Run: release-it-preset init');
|
|
337
|
+
console.error(' 2. Run: release-it-preset <command>\n');
|
|
338
|
+
showHelp();
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const config = JSON.parse(readFileSync(userConfigPath, 'utf8'));
|
|
344
|
+
const extendsMatch = config.extends?.match(/@oorabona\/release-it-preset\/config\/([\w-]+)/);
|
|
345
|
+
|
|
346
|
+
if (!extendsMatch) {
|
|
347
|
+
console.error('❌ .release-it.json does not extend a known preset\n');
|
|
348
|
+
console.error('Expected extends field like:');
|
|
349
|
+
console.error(' "@oorabona/release-it-preset/config/default"\n');
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const preset = extendsMatch[1];
|
|
354
|
+
console.log(`🔍 Auto-detected preset: ${preset}`);
|
|
355
|
+
handleReleaseCommand(preset, []);
|
|
356
|
+
return;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error(`❌ Error reading .release-it.json: ${error.message}`);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// MODE 3: Preset command (existing logic)
|
|
252
364
|
if (RELEASE_CONFIGS[command]) {
|
|
253
365
|
handleReleaseCommand(command, commandArgs);
|
|
254
366
|
return;
|
|
255
367
|
}
|
|
256
368
|
|
|
257
|
-
//
|
|
369
|
+
// MODE 4: Utility command (existing logic)
|
|
258
370
|
if (UTILITY_COMMANDS[command]) {
|
|
259
371
|
handleUtilityCommand(command, commandArgs);
|
|
260
372
|
return;
|
|
@@ -264,6 +376,7 @@ function main() {
|
|
|
264
376
|
console.error(`❌ Unknown command: ${command}`);
|
|
265
377
|
console.error(`\nAvailable release configs: ${Object.keys(RELEASE_CONFIGS).join(', ')}`);
|
|
266
378
|
console.error(`Available utility commands: ${Object.keys(UTILITY_COMMANDS).join(', ')}`);
|
|
379
|
+
console.error(`\nFor direct config file usage: release-it-preset --config <file>`);
|
|
267
380
|
console.error(`\nRun 'release-it-preset --help' for more information.`);
|
|
268
381
|
process.exit(1);
|
|
269
382
|
}
|
package/bin/validators.js
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* - Fail securely
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { existsSync, statSync } from 'node:fs';
|
|
14
|
+
import { extname, isAbsolute, resolve } from 'node:path';
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* Validates that a config name is in the allowed list
|
|
15
18
|
*
|
|
@@ -96,26 +99,75 @@ export function sanitizeArgs(args) {
|
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
/**
|
|
99
|
-
* Validates
|
|
102
|
+
* Validates config file paths with monorepo support
|
|
103
|
+
*
|
|
104
|
+
* Security approach: Defense in depth with multiple validation layers
|
|
105
|
+
* - Whitelist allowed file extensions
|
|
106
|
+
* - Limit parent directory traversal depth (monorepo support)
|
|
107
|
+
* - Reject absolute paths from CLI
|
|
108
|
+
* - Validate file existence
|
|
109
|
+
*
|
|
110
|
+
* Why we allow ".." (parent directory references):
|
|
111
|
+
* - Standard pattern in monorepos (TypeScript, ESLint, Prettier all allow it)
|
|
112
|
+
* - Developer controls the environment (config files are trusted code boundary)
|
|
113
|
+
* - No privilege escalation possible in CLI tool context
|
|
114
|
+
* - Multiple validation layers prevent abuse
|
|
100
115
|
*
|
|
101
|
-
* @param {string}
|
|
102
|
-
* @throws {Error} If
|
|
103
|
-
* @returns {string}
|
|
116
|
+
* @param {string} configPath - The config file path to validate (relative or absolute)
|
|
117
|
+
* @throws {Error} If validation fails (invalid extension, too deep, missing file, etc.)
|
|
118
|
+
* @returns {string} Absolute path to validated config file
|
|
104
119
|
*/
|
|
105
|
-
export function
|
|
106
|
-
//
|
|
107
|
-
|
|
120
|
+
export function validateConfigPath(configPath) {
|
|
121
|
+
// 1. Whitelist config file extensions (defense in depth)
|
|
122
|
+
const allowedExtensions = ['.json', '.js', '.cjs', '.mjs', '.yaml', '.yml', '.toml'];
|
|
123
|
+
const ext = extname(configPath).toLowerCase();
|
|
124
|
+
|
|
125
|
+
if (!allowedExtensions.includes(ext)) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Invalid config file extension: "${ext}"\n` +
|
|
128
|
+
`Allowed: ${allowedExtensions.join(', ')}\n` +
|
|
129
|
+
`This restriction prevents reading non-config files.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. Limit parent directory traversal depth (max 5 levels for monorepo support)
|
|
134
|
+
const upwardLevels = (configPath.match(/\.\.\//g) || []).length;
|
|
135
|
+
if (upwardLevels > 5) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Too many parent directory references: ${upwardLevels}\n` +
|
|
138
|
+
`Maximum allowed: 5 levels (../../../../../../)\n` +
|
|
139
|
+
`This prevents accidental access to system directories.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 3. Reject absolute paths from CLI (could reference system files)
|
|
144
|
+
if (isAbsolute(configPath)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Absolute paths not allowed: "${configPath}"\n` +
|
|
147
|
+
`Use relative paths from your project directory.\n` +
|
|
148
|
+
`For monorepos, use parent references like ../../config.json`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 4. Resolve to absolute path and validate file exists
|
|
153
|
+
const resolved = resolve(process.cwd(), configPath);
|
|
154
|
+
|
|
155
|
+
if (!existsSync(resolved)) {
|
|
108
156
|
throw new Error(
|
|
109
|
-
`
|
|
157
|
+
`Config file not found: "${configPath}"\n` +
|
|
158
|
+
`Resolved to: ${resolved}\n` +
|
|
159
|
+
`Check that the file exists and the path is correct.`
|
|
110
160
|
);
|
|
111
161
|
}
|
|
112
162
|
|
|
113
|
-
//
|
|
114
|
-
|
|
163
|
+
// 5. Validate it's a file (not a directory or symlink to avoid confusion)
|
|
164
|
+
const stats = statSync(resolved);
|
|
165
|
+
if (!stats.isFile()) {
|
|
115
166
|
throw new Error(
|
|
116
|
-
`
|
|
167
|
+
`Config path must be a file, not a directory: "${configPath}"\n` +
|
|
168
|
+
`Resolved to: ${resolved}`
|
|
117
169
|
);
|
|
118
170
|
}
|
|
119
171
|
|
|
120
|
-
return
|
|
172
|
+
return resolved;
|
|
121
173
|
}
|
package/config/constants.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Git configuration defaults
|
|
14
14
|
*/
|
|
15
15
|
export const GIT_DEFAULTS = {
|
|
16
|
-
COMMIT_MESSAGE: 'release:
|
|
17
|
-
HOTFIX_COMMIT_MESSAGE: 'hotfix:
|
|
16
|
+
COMMIT_MESSAGE: 'chore(release): v${version}',
|
|
17
|
+
HOTFIX_COMMIT_MESSAGE: 'chore(hotfix): v${version}',
|
|
18
18
|
TAG_NAME: 'v${version}',
|
|
19
19
|
REQUIRE_BRANCH: 'main',
|
|
20
20
|
REMOTE: 'origin',
|
|
@@ -22,7 +22,9 @@ export const GIT_DEFAULTS = {
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Default git changelog command
|
|
25
|
-
* Filters out commits matching release/hotfix/ci patterns
|
|
25
|
+
* Filters out commits matching release/hotfix/ci patterns, in both
|
|
26
|
+
* legacy form (`release: bump ...`) and Conventional Commits form
|
|
27
|
+
* (`chore(release): ...`, `chore(hotfix): ...`).
|
|
26
28
|
*/
|
|
27
29
|
export const DEFAULT_CHANGELOG_COMMAND = [
|
|
28
30
|
'git log',
|
|
@@ -36,6 +38,9 @@ export const DEFAULT_CHANGELOG_COMMAND = [
|
|
|
36
38
|
'--grep="^Hotfix"',
|
|
37
39
|
'--grep="^ci"',
|
|
38
40
|
'--grep="^CI"',
|
|
41
|
+
'--grep="^chore(release)"',
|
|
42
|
+
'--grep="^chore(hotfix)"',
|
|
43
|
+
'--grep="^chore(ci)"',
|
|
39
44
|
'--invert-grep',
|
|
40
45
|
].join(' ');
|
|
41
46
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { execSync } from 'node:child_process';
|
|
16
16
|
import { existsSync, readFileSync } from 'node:fs';
|
|
17
17
|
import { getGitHubRepoUrl } from './lib/git-utils.js';
|
|
18
|
+
import { runScript } from './lib/run-script.js';
|
|
18
19
|
export function safeExec(command, deps) {
|
|
19
20
|
try {
|
|
20
21
|
return deps.execSync(command, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
@@ -256,7 +257,7 @@ export function displayNpmStatus(deps, npmUsername) {
|
|
|
256
257
|
*/
|
|
257
258
|
/* c8 ignore start */
|
|
258
259
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
259
|
-
async
|
|
260
|
+
void runScript({ error: console.error, exit: process.exit }, async () => {
|
|
260
261
|
console.log('🔍 Checking release configuration and project status...');
|
|
261
262
|
const deps = {
|
|
262
263
|
execSync,
|
|
@@ -276,10 +277,6 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
276
277
|
console.log('\n✨ Check complete!');
|
|
277
278
|
console.log('\nTo validate release readiness, run:');
|
|
278
279
|
console.log(' pnpm release-it-preset validate\n');
|
|
279
|
-
}
|
|
280
|
-
main().catch((error) => {
|
|
281
|
-
console.error('❌ Check failed:', error);
|
|
282
|
-
process.exit(1);
|
|
283
280
|
});
|
|
284
281
|
}
|
|
285
282
|
/* c8 ignore end */
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { execSync } from 'node:child_process';
|
|
14
14
|
import { appendFileSync } from 'node:fs';
|
|
15
15
|
import { STRICT_CONVENTIONAL_COMMIT_REGEX } from './lib/commit-parser.js';
|
|
16
|
+
import { runScript } from './lib/run-script.js';
|
|
16
17
|
const SKIP_CHANGELOG_REGEX = /\[skip-changelog]/i;
|
|
17
18
|
export function safeExec(command, deps) {
|
|
18
19
|
try {
|
|
@@ -155,10 +156,12 @@ export function renderSummary(result, deps) {
|
|
|
155
156
|
}
|
|
156
157
|
/* c8 ignore start */
|
|
157
158
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
void runScript({ error: console.error, exit: process.exit }, () => {
|
|
160
|
+
const deps = createDefaultDeps();
|
|
161
|
+
const args = parseArgs(process.argv.slice(2));
|
|
162
|
+
const result = runPrCheck(args, deps);
|
|
163
|
+
writeOutputs(result, deps);
|
|
164
|
+
renderSummary(result, deps);
|
|
165
|
+
});
|
|
163
166
|
}
|
|
164
167
|
/* c8 ignore end */
|
|
@@ -18,6 +18,7 @@ import { readFileSync } from 'node:fs';
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { validateAndNormalizeSemver } from './lib/semver-utils.js';
|
|
20
20
|
import { escapeRegExp } from './lib/string-utils.js';
|
|
21
|
+
import { runScript } from './lib/run-script.js';
|
|
21
22
|
export function extractChangelog(version, deps) {
|
|
22
23
|
// Validate semver format
|
|
23
24
|
const normalizedVersion = validateAndNormalizeSemver(version);
|
|
@@ -45,22 +46,19 @@ export function extractChangelog(version, deps) {
|
|
|
45
46
|
*/
|
|
46
47
|
/* c8 ignore start */
|
|
47
48
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
void runScript({ error: console.error, exit: process.exit }, () => {
|
|
50
|
+
const version = process.argv[2];
|
|
51
|
+
if (!version) {
|
|
52
|
+
console.error('Usage: tsx scripts/extract-changelog.ts <version>');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
54
56
|
const result = extractChangelog(version, {
|
|
55
57
|
readFileSync,
|
|
56
58
|
getEnv: (key) => process.env[key],
|
|
57
59
|
getCwd: () => process.cwd(),
|
|
58
60
|
});
|
|
59
61
|
console.log(result);
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
console.error(`❌ ${error instanceof Error ? error.message : error}`);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
62
|
+
});
|
|
65
63
|
}
|
|
66
64
|
/* c8 ignore end */
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
17
|
import { createInterface } from 'node:readline';
|
|
18
|
+
import { runScript } from './lib/run-script.js';
|
|
18
19
|
const CHANGELOG_TEMPLATE = `# Changelog
|
|
19
20
|
|
|
20
21
|
All notable changes to this project will be documented in this file.
|
|
@@ -163,29 +164,28 @@ export async function initProject(options, deps) {
|
|
|
163
164
|
*/
|
|
164
165
|
/* c8 ignore start */
|
|
165
166
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
166
|
-
async
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
rl.
|
|
174
|
-
|
|
167
|
+
void runScript({ error: console.error, exit: process.exit }, async () => {
|
|
168
|
+
async function realPrompt(question) {
|
|
169
|
+
const rl = createInterface({
|
|
170
|
+
input: process.stdin,
|
|
171
|
+
output: process.stdout,
|
|
172
|
+
});
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
rl.question(`${question} (y/N): `, (answer) => {
|
|
175
|
+
rl.close();
|
|
176
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
177
|
+
});
|
|
175
178
|
});
|
|
179
|
+
}
|
|
180
|
+
const options = parseArgs();
|
|
181
|
+
await initProject(options, {
|
|
182
|
+
existsSync,
|
|
183
|
+
readFileSync,
|
|
184
|
+
writeFileSync,
|
|
185
|
+
prompt: realPrompt,
|
|
186
|
+
log: console.log,
|
|
187
|
+
warn: console.warn,
|
|
176
188
|
});
|
|
177
|
-
}
|
|
178
|
-
const options = parseArgs();
|
|
179
|
-
initProject(options, {
|
|
180
|
-
existsSync,
|
|
181
|
-
readFileSync,
|
|
182
|
-
writeFileSync,
|
|
183
|
-
prompt: realPrompt,
|
|
184
|
-
log: console.log,
|
|
185
|
-
warn: console.warn,
|
|
186
|
-
}).catch((error) => {
|
|
187
|
-
console.error('❌ Initialization failed:', error);
|
|
188
|
-
process.exit(1);
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
191
|
/* c8 ignore end */
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error hierarchy for script-level failures.
|
|
3
|
+
*
|
|
4
|
+
* Each subclass maps to a specific CLI exit code so that callers and
|
|
5
|
+
* the `runScript()` wrapper can surface the right exit code without
|
|
6
|
+
* inspecting raw strings.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* throw new ValidationError('CHANGELOG.md not found')
|
|
10
|
+
* // → exits with code 2
|
|
11
|
+
*
|
|
12
|
+
* throw new GitError('git tag not found')
|
|
13
|
+
* // → exits with code 1
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Base class for all script-level errors that map to a CLI exit code.
|
|
17
|
+
* Throw a subclass to signal a specific failure mode; uncaught throws
|
|
18
|
+
* fall through to exit code 1 via runScript().
|
|
19
|
+
*/
|
|
20
|
+
export class ScriptError extends Error {
|
|
21
|
+
exitCode;
|
|
22
|
+
constructor(message, options) {
|
|
23
|
+
super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);
|
|
24
|
+
this.name = this.constructor.name;
|
|
25
|
+
this.exitCode = options?.exitCode ?? 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Pre-flight or input-validation failure (e.g. missing CHANGELOG, wrong branch, dirty tree). Exit 2. */
|
|
29
|
+
export class ValidationError extends ScriptError {
|
|
30
|
+
constructor(message, options) {
|
|
31
|
+
super(message, { exitCode: 2, cause: options?.cause });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Git operation failure (tag not found, command non-zero, parse error on git output). Exit 1. */
|
|
35
|
+
export class GitError extends ScriptError {
|
|
36
|
+
constructor(message, options) {
|
|
37
|
+
super(message, { exitCode: 1, cause: options?.cause });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Changelog parse/write failure (malformed file, missing [Unreleased], etc.). Exit 1. */
|
|
41
|
+
export class ChangelogError extends ScriptError {
|
|
42
|
+
constructor(message, options) {
|
|
43
|
+
super(message, { exitCode: 1, cause: options?.cause });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ScriptError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps a script's main function with consistent error handling and
|
|
4
|
+
* exit-code mapping. ScriptError subclasses exit with their specific
|
|
5
|
+
* exitCode; any other thrown value exits 1 with a generic message.
|
|
6
|
+
*
|
|
7
|
+
* The function may be sync or async. Returns a promise that resolves
|
|
8
|
+
* before exit() is called for the failure path; on success the promise
|
|
9
|
+
* simply resolves (exit() is not invoked — caller can let process end
|
|
10
|
+
* naturally with code 0).
|
|
11
|
+
*/
|
|
12
|
+
export async function runScript(deps, fn) {
|
|
13
|
+
try {
|
|
14
|
+
await fn();
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
if (err instanceof ScriptError) {
|
|
18
|
+
deps.error(`❌ ${err.message}`);
|
|
19
|
+
deps.exit(err.exitCode);
|
|
20
|
+
}
|
|
21
|
+
if (err instanceof Error) {
|
|
22
|
+
deps.error(`❌ Unexpected error: ${err.message}`);
|
|
23
|
+
deps.exit(1);
|
|
24
|
+
}
|
|
25
|
+
deps.error(`❌ Unknown error: ${String(err)}`);
|
|
26
|
+
deps.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -21,6 +21,7 @@ import { execSync } from 'node:child_process';
|
|
|
21
21
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
22
22
|
import { getGitHubRepoUrl } from './lib/git-utils.js';
|
|
23
23
|
import { CONVENTIONAL_COMMIT_REGEX } from './lib/commit-parser.js';
|
|
24
|
+
import { runScript } from './lib/run-script.js';
|
|
24
25
|
/**
|
|
25
26
|
* Extract all conventional commit patterns from a commit body
|
|
26
27
|
*/
|
|
@@ -98,7 +99,14 @@ export function parseCommitsWithMultiplePrefixes(gitOutput, repoUrl) {
|
|
|
98
99
|
const firstLine = body.split('\n')[0].trim();
|
|
99
100
|
if (firstLine) {
|
|
100
101
|
const lowerFirstLine = firstLine.toLowerCase();
|
|
101
|
-
const ignoredPatterns = [
|
|
102
|
+
const ignoredPatterns = [
|
|
103
|
+
/^release\b/,
|
|
104
|
+
/^hotfix\b/,
|
|
105
|
+
/^ci\b/,
|
|
106
|
+
/^chore\(release\)/i,
|
|
107
|
+
/^chore\(hotfix\)/i,
|
|
108
|
+
/^chore\(ci\)/i,
|
|
109
|
+
];
|
|
102
110
|
const shouldSkip = ignoredPatterns.some((pattern) => pattern.test(lowerFirstLine));
|
|
103
111
|
if (shouldSkip) {
|
|
104
112
|
continue;
|
|
@@ -223,20 +231,14 @@ export function populateChangelog(deps) {
|
|
|
223
231
|
*/
|
|
224
232
|
/* c8 ignore start */
|
|
225
233
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
console.error('❌ Failed to populate [Unreleased] section:', error);
|
|
239
|
-
process.exit(1);
|
|
240
|
-
}
|
|
234
|
+
void runScript({ error: console.error, exit: process.exit }, () => populateChangelog({
|
|
235
|
+
execSync,
|
|
236
|
+
readFileSync,
|
|
237
|
+
writeFileSync,
|
|
238
|
+
getEnv: (key) => process.env[key],
|
|
239
|
+
log: console.log,
|
|
240
|
+
warn: console.warn,
|
|
241
|
+
error: console.error,
|
|
242
|
+
}));
|
|
241
243
|
}
|
|
242
244
|
/* c8 ignore end */
|
|
@@ -24,6 +24,7 @@ import { execSync } from 'node:child_process';
|
|
|
24
24
|
import { getGitHubRepoUrl } from './lib/git-utils.js';
|
|
25
25
|
import { escapeRegExp } from './lib/string-utils.js';
|
|
26
26
|
import { validateAndNormalizeSemver } from './lib/semver-utils.js';
|
|
27
|
+
import { runScript } from './lib/run-script.js';
|
|
27
28
|
export function updateReferenceLinks(changelog, versionLabels, linkTarget, unreleasedLine) {
|
|
28
29
|
const lines = changelog.split(/\r?\n/);
|
|
29
30
|
const updatedLines = [];
|
|
@@ -162,26 +163,19 @@ export function republishChangelog(version, deps) {
|
|
|
162
163
|
*/
|
|
163
164
|
/* c8 ignore start */
|
|
164
165
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
165
|
-
async
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
console.error(`❌ ${error instanceof Error ? error.message : error}`);
|
|
182
|
-
process.exit(1);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
main();
|
|
166
|
+
void runScript({ error: console.error, exit: process.exit }, async () => {
|
|
167
|
+
const pkg = await import(join(process.cwd(), 'package.json'), { with: { type: 'json' } });
|
|
168
|
+
const version = pkg.default.version;
|
|
169
|
+
republishChangelog(version, {
|
|
170
|
+
execSync,
|
|
171
|
+
readFileSync,
|
|
172
|
+
writeFileSync,
|
|
173
|
+
getEnv: (key) => process.env[key],
|
|
174
|
+
getCwd: () => process.cwd(),
|
|
175
|
+
getDate: () => new Date().toISOString().split('T')[0],
|
|
176
|
+
log: console.log,
|
|
177
|
+
warn: console.warn,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
186
180
|
}
|
|
187
181
|
/* c8 ignore end */
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { execSync } from 'node:child_process';
|
|
19
19
|
import { readFileSync } from 'node:fs';
|
|
20
|
+
import { runScript } from './lib/run-script.js';
|
|
20
21
|
export function retryPublish(deps) {
|
|
21
22
|
deps.log('🔄 Starting retry publish process...');
|
|
22
23
|
const currentBranch = deps.execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
|
@@ -82,17 +83,13 @@ export function retryPublish(deps) {
|
|
|
82
83
|
*/
|
|
83
84
|
/* c8 ignore start */
|
|
84
85
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
85
|
-
|
|
86
|
+
void runScript({ error: console.error, exit: process.exit }, () => {
|
|
86
87
|
retryPublish({
|
|
87
88
|
execSync,
|
|
88
89
|
readFileSync,
|
|
89
90
|
log: console.log,
|
|
90
91
|
warn: console.warn,
|
|
91
92
|
});
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
console.error(`❌ Pre-flight checks failed: ${error instanceof Error ? error.message : error}`);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
93
|
+
});
|
|
97
94
|
}
|
|
98
95
|
/* c8 ignore end */
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { execSync } from 'node:child_process';
|
|
20
20
|
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import { ValidationError } from './lib/errors.js';
|
|
22
|
+
import { runScript } from './lib/run-script.js';
|
|
21
23
|
export function parseArgs(argv) {
|
|
22
24
|
return {
|
|
23
25
|
allowDirty: argv.includes('--allow-dirty'),
|
|
@@ -248,7 +250,7 @@ export function validateRelease(deps, options) {
|
|
|
248
250
|
*/
|
|
249
251
|
/* c8 ignore start */
|
|
250
252
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
251
|
-
async
|
|
253
|
+
void runScript({ error: console.error, exit: process.exit }, async () => {
|
|
252
254
|
const options = parseArgs(process.argv.slice(2));
|
|
253
255
|
console.log('🔍 Validating release readiness...\n');
|
|
254
256
|
const deps = {
|
|
@@ -273,16 +275,10 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
273
275
|
console.log();
|
|
274
276
|
if (allPassed) {
|
|
275
277
|
console.log('✨ All validations passed! Ready to release.');
|
|
276
|
-
process.exit(0);
|
|
277
278
|
}
|
|
278
279
|
else {
|
|
279
|
-
|
|
280
|
-
process.exit(1);
|
|
280
|
+
throw new ValidationError('Some validations failed. Please fix the issues above before releasing.');
|
|
281
281
|
}
|
|
282
|
-
}
|
|
283
|
-
main().catch((error) => {
|
|
284
|
-
console.error('❌ Validation failed:', error);
|
|
285
|
-
process.exit(1);
|
|
286
282
|
});
|
|
287
283
|
}
|
|
288
284
|
/* c8 ignore end */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error hierarchy for script-level failures.
|
|
3
|
+
*
|
|
4
|
+
* Each subclass maps to a specific CLI exit code so that callers and
|
|
5
|
+
* the `runScript()` wrapper can surface the right exit code without
|
|
6
|
+
* inspecting raw strings.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* throw new ValidationError('CHANGELOG.md not found')
|
|
10
|
+
* // → exits with code 2
|
|
11
|
+
*
|
|
12
|
+
* throw new GitError('git tag not found')
|
|
13
|
+
* // → exits with code 1
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Base class for all script-level errors that map to a CLI exit code.
|
|
17
|
+
* Throw a subclass to signal a specific failure mode; uncaught throws
|
|
18
|
+
* fall through to exit code 1 via runScript().
|
|
19
|
+
*/
|
|
20
|
+
export declare class ScriptError extends Error {
|
|
21
|
+
readonly exitCode: number;
|
|
22
|
+
constructor(message: string, options?: {
|
|
23
|
+
exitCode?: number;
|
|
24
|
+
cause?: unknown;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** Pre-flight or input-validation failure (e.g. missing CHANGELOG, wrong branch, dirty tree). Exit 2. */
|
|
28
|
+
export declare class ValidationError extends ScriptError {
|
|
29
|
+
constructor(message: string, options?: {
|
|
30
|
+
cause?: unknown;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Git operation failure (tag not found, command non-zero, parse error on git output). Exit 1. */
|
|
34
|
+
export declare class GitError extends ScriptError {
|
|
35
|
+
constructor(message: string, options?: {
|
|
36
|
+
cause?: unknown;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Changelog parse/write failure (malformed file, missing [Unreleased], etc.). Exit 1. */
|
|
40
|
+
export declare class ChangelogError extends ScriptError {
|
|
41
|
+
constructor(message: string, options?: {
|
|
42
|
+
cause?: unknown;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface RunScriptDeps {
|
|
2
|
+
error: (msg: string) => void;
|
|
3
|
+
exit: (code: number) => never;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a script's main function with consistent error handling and
|
|
7
|
+
* exit-code mapping. ScriptError subclasses exit with their specific
|
|
8
|
+
* exitCode; any other thrown value exits 1 with a generic message.
|
|
9
|
+
*
|
|
10
|
+
* The function may be sync or async. Returns a promise that resolves
|
|
11
|
+
* before exit() is called for the failure path; on success the promise
|
|
12
|
+
* simply resolves (exit() is not invoked — caller can let process end
|
|
13
|
+
* naturally with code 0).
|
|
14
|
+
*/
|
|
15
|
+
export declare function runScript(deps: RunScriptDeps, fn: () => void | Promise<void>): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oorabona/release-it-preset",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Shared release-it configuration and scripts for the organisation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -69,23 +69,25 @@
|
|
|
69
69
|
"test:unit": "vitest run tests/unit/*.test.ts",
|
|
70
70
|
"test:unit:watch": "vitest tests/unit/*.test.ts",
|
|
71
71
|
"test:unit:coverage": "vitest run tests/unit/*.test.ts --coverage",
|
|
72
|
+
"test:e2e": "vitest run tests/e2e/*.test.ts --testTimeout=30000",
|
|
72
73
|
"test:watch": "vitest",
|
|
73
74
|
"test:coverage": "vitest run --coverage",
|
|
74
75
|
"test:ui": "vitest --ui",
|
|
75
76
|
"prepublishOnly": "pnpm build && echo 'Running prepublish checks...' && test -f README.md && test -f LICENSE"
|
|
76
77
|
},
|
|
77
78
|
"peerDependencies": {
|
|
78
|
-
"release-it": "^
|
|
79
|
+
"release-it": "^20.0.0"
|
|
79
80
|
},
|
|
80
81
|
"devDependencies": {
|
|
81
|
-
"@biomejs/biome": "^2.
|
|
82
|
-
"@types/node": "^
|
|
83
|
-
"@vitest/coverage-v8": "^
|
|
84
|
-
"nano-staged": "^0.
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
82
|
+
"@biomejs/biome": "^2.4.13",
|
|
83
|
+
"@types/node": "^25.6.0",
|
|
84
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
85
|
+
"nano-staged": "^1.0.2",
|
|
86
|
+
"release-it": "^20.0.1",
|
|
87
|
+
"rimraf": "^6.1.3",
|
|
88
|
+
"tsx": "^4.21.0",
|
|
89
|
+
"typescript": "^6.0.3",
|
|
90
|
+
"vitest": "^4.1.5"
|
|
89
91
|
},
|
|
90
92
|
"engines": {
|
|
91
93
|
"node": ">=18.0.0"
|