@oorabona/release-it-preset 0.8.1 → 0.9.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/README.md CHANGED
@@ -16,6 +16,11 @@ Shared [release-it](https://github.com/release-it/release-it) configuration and
16
16
  - [Quick Start](#quick-start)
17
17
  - [Available Configurations](#available-configurations)
18
18
  - [CLI Usage](#cli-usage)
19
+ - [Zero-Config Mode (Auto-Detection)](#zero-config-mode-auto-detection)
20
+ - [Preset Selection Mode](#preset-selection-mode)
21
+ - [Passthrough Mode (Custom Config Override)](#passthrough-mode-custom-config-override)
22
+ - [Monorepo Support](#monorepo-support)
23
+ - [Utility Commands](#utility-commands)
19
24
  - [Scripts](#scripts)
20
25
  - [Environment Variables](#environment-variables)
21
26
  - [Configuration Override](#configuration-override)
@@ -37,6 +42,9 @@ Shared [release-it](https://github.com/release-it/release-it) configuration and
37
42
  - 🔄 Republish and retry mechanisms for failed releases
38
43
  - ⚡ Hotfix release support
39
44
  - 🎯 Environment variable configuration
45
+ - 🔍 **NEW v0.9.0:** Zero-config auto-detection mode
46
+ - 🏢 **NEW v0.9.0:** Monorepo support with parent directory config references
47
+ - ⚙️ **NEW v0.9.0:** Passthrough mode for custom config files
40
48
 
41
49
  ## Installation
42
50
 
@@ -331,9 +339,42 @@ Features:
331
339
 
332
340
  ## CLI Usage
333
341
 
334
- The package provides a `release-it-preset` CLI with two types of commands:
342
+ The package provides a `release-it-preset` CLI with four operating modes:
335
343
 
336
- ### Release Commands
344
+ 1. **Zero-Config Mode** (auto-detection) - No arguments needed
345
+ 2. **Preset Selection Mode** - Specify which preset to use
346
+ 3. **Passthrough Mode** - Direct config file override
347
+ 4. **Utility Mode** - Helper commands
348
+
349
+ ### Zero-Config Mode (Auto-Detection)
350
+
351
+ **NEW in v0.9.0** - The CLI can automatically detect which preset to use from your `.release-it.json`:
352
+
353
+ ```bash
354
+ # Just run release-it-preset with no arguments
355
+ pnpm release-it-preset
356
+
357
+ # 🔍 Auto-detected preset: default
358
+ # ✅ Config validated: preset "default"
359
+ # 📝 Using: /path/to/.release-it.json
360
+ ```
361
+
362
+ **How it works:**
363
+ 1. CLI reads your `.release-it.json`
364
+ 2. Extracts the preset name from the `extends` field
365
+ 3. Runs that preset automatically
366
+
367
+ **Requirements:**
368
+ - `.release-it.json` must exist
369
+ - Must have `extends` field like `"@oorabona/release-it-preset/config/default"`
370
+
371
+ **Benefits:**
372
+ - ✅ Shortest command possible
373
+ - ✅ Config file is source of truth
374
+ - ✅ No need to remember preset names
375
+ - ✅ Follows industry standards (ESLint, TypeScript, Prettier)
376
+
377
+ ### Preset Selection Mode
337
378
 
338
379
  Run release-it with specific configurations:
339
380
 
@@ -350,6 +391,92 @@ pnpm release-it-preset manual-changelog
350
391
 
351
392
  All additional arguments are passed through to release-it.
352
393
 
394
+ ### Passthrough Mode (Custom Config Override)
395
+
396
+ **NEW in v0.9.0** - Use a custom config file and bypass preset validation:
397
+
398
+ ```bash
399
+ # Use custom config file
400
+ pnpm release-it-preset --config .release-it-manual.json
401
+
402
+ # 🔀 Passthrough mode: using config .release-it-manual.json
403
+ # Bypassing preset validation - direct release-it invocation
404
+ ```
405
+
406
+ **Use cases:**
407
+ - **Switching presets occasionally** - Have multiple config files for different scenarios
408
+ - **Monorepo workflows** - Reference shared configs from parent directories
409
+ - **Advanced customization** - Full control over release-it configuration
410
+
411
+ **Example workflow:**
412
+
413
+ ```json
414
+ // .release-it.json (default - 95% of time)
415
+ {
416
+ "extends": "@oorabona/release-it-preset/config/default",
417
+ "git": { "requireBranch": "develop" }
418
+ }
419
+
420
+ // .release-it-manual.json (rare - 5% of time)
421
+ {
422
+ "extends": "@oorabona/release-it-preset/config/manual-changelog",
423
+ "git": { "requireBranch": "develop" }
424
+ }
425
+ ```
426
+
427
+ ```bash
428
+ # Normal release
429
+ pnpm release-it-preset # Auto-detects default
430
+
431
+ # Manual changelog release (rare)
432
+ pnpm release-it-preset --config .release-it-manual.json
433
+ ```
434
+
435
+ **Benefits:**
436
+ - ✅ No need to edit `.release-it.json` to switch presets
437
+ - ✅ Config files are explicit and version-controlled
438
+ - ✅ Works with monorepo parent directory references
439
+
440
+ ### Monorepo Support
441
+
442
+ **NEW in v0.9.0** - Parent directory config references are now supported:
443
+
444
+ ```bash
445
+ # Monorepo structure
446
+ /my-monorepo/
447
+ ├── .release-it-base.json # Shared configuration
448
+ ├── packages/
449
+ │ ├── core/
450
+ │ │ └── .release-it.json # extends: ../../.release-it-base.json
451
+ │ └── utils/
452
+ │ └── .release-it.json # extends: ../../.release-it-base.json
453
+ ```
454
+
455
+ ```json
456
+ // packages/core/.release-it.json
457
+ {
458
+ "extends": [
459
+ "../../.release-it-base.json", // ✅ Parent reference allowed!
460
+ "@oorabona/release-it-preset/config/default"
461
+ ]
462
+ }
463
+ ```
464
+
465
+ **Security validation:**
466
+ - ✅ Parent directory references (`../`) supported (up to 5 levels)
467
+ - ✅ Config file extension whitelist (`.json`, `.js`, `.cjs`, `.mjs`, `.yaml`, `.yml`, `.toml`)
468
+ - ✅ File existence validation
469
+ - ❌ Absolute paths blocked (use relative paths)
470
+ - ❌ Excessive traversal blocked (max `../../../../../../`)
471
+
472
+ **Why this is safe:**
473
+ - Config files are trusted code (developer controls the repository)
474
+ - Industry standard pattern (TypeScript, ESLint, Prettier all allow `../`)
475
+ - Multiple validation layers prevent abuse
476
+ - No privilege escalation in CLI tool context
477
+
478
+ See [examples/monorepo-workflow.md](examples/monorepo-workflow.md) for complete monorepo guide.
479
+
353
480
  ### Utility Commands
354
481
 
355
482
  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 <command> [...args]
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
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
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 if it's a release config
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
- // Check if it's a utility command
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 that a path does not contain directory traversal attempts
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} path - The path to validate
102
- * @throws {Error} If path contains traversal patterns
103
- * @returns {string} The validated path
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 validatePath(path) {
106
- // Check for directory traversal patterns
107
- if (path.includes('..')) {
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
- `Path contains directory traversal pattern (..) which is not allowed: "${path}"`
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
- // Check for absolute paths (we expect relative paths)
114
- if (path.startsWith('/') || /^[a-zA-Z]:/.test(path)) {
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
- `Absolute paths are not allowed: "${path}"`
167
+ `Config path must be a file, not a directory: "${configPath}"\n` +
168
+ `Resolved to: ${resolved}`
117
169
  );
118
170
  }
119
171
 
120
- return path;
172
+ return resolved;
121
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oorabona/release-it-preset",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Shared release-it configuration and scripts for the organisation",
5
5
  "type": "module",
6
6
  "keywords": [