@oorabona/release-it-preset 0.9.0 → 0.10.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
@@ -4,6 +4,7 @@ Shared [release-it](https://github.com/release-it/release-it) configuration and
4
4
 
5
5
  [![codecov](https://codecov.io/github/oorabona/release-it-preset/graph/badge.svg?token=6RMN34Z7TX)](https://codecov.io/github/oorabona/release-it-preset)
6
6
  [![CI](https://github.com/oorabona/release-it-preset/actions/workflows/ci.yml/badge.svg)](https://github.com/oorabona/release-it-preset/actions/workflows/ci.yml)
7
+ [![Audit](https://github.com/oorabona/release-it-preset/actions/workflows/audit.yml/badge.svg)](https://github.com/oorabona/release-it-preset/actions/workflows/audit.yml)
7
8
  [![NPM Version](https://img.shields.io/npm/v/release-it-preset.svg)](https://npmjs.org/package/@oorabona/release-it-preset)
8
9
  [![NPM Downloads](https://img.shields.io/npm/dm/release-it-preset.svg)](https://npmjs.org/package/@oorabona/release-it-preset)
9
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.3+-blue.svg)](https://www.typescriptlang.org/)
@@ -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 function main() {
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
- const deps = createDefaultDeps();
159
- const args = parseArgs(process.argv.slice(2));
160
- const result = runPrCheck(args, deps);
161
- writeOutputs(result, deps);
162
- renderSummary(result, deps);
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
- const version = process.argv[2];
49
- if (!version) {
50
- console.error('Usage: tsx scripts/extract-changelog.ts <version>');
51
- process.exit(1);
52
- }
53
- try {
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 function realPrompt(question) {
167
- const rl = createInterface({
168
- input: process.stdin,
169
- output: process.stdout,
170
- });
171
- return new Promise((resolve) => {
172
- rl.question(`${question} (y/N): `, (answer) => {
173
- rl.close();
174
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
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
  */
@@ -223,20 +224,14 @@ export function populateChangelog(deps) {
223
224
  */
224
225
  /* c8 ignore start */
225
226
  if (import.meta.url === `file://${process.argv[1]}`) {
226
- try {
227
- populateChangelog({
228
- execSync,
229
- readFileSync,
230
- writeFileSync,
231
- getEnv: (key) => process.env[key],
232
- log: console.log,
233
- warn: console.warn,
234
- error: console.error,
235
- });
236
- }
237
- catch (error) {
238
- console.error('āŒ Failed to populate [Unreleased] section:', error);
239
- process.exit(1);
240
- }
227
+ void runScript({ error: console.error, exit: process.exit }, () => populateChangelog({
228
+ execSync,
229
+ readFileSync,
230
+ writeFileSync,
231
+ getEnv: (key) => process.env[key],
232
+ log: console.log,
233
+ warn: console.warn,
234
+ error: console.error,
235
+ }));
241
236
  }
242
237
  /* 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 function main() {
166
- try {
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
- }
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
- try {
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 function main() {
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
- console.log('āŒ Some validations failed. Please fix the issues above before releasing.');
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.9.0",
3
+ "version": "0.10.0",
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": "^19.0.0"
79
+ "release-it": "^20.0.0"
79
80
  },
80
81
  "devDependencies": {
81
- "@biomejs/biome": "^2.2.5",
82
- "@types/node": "^24.6.2",
83
- "@vitest/coverage-v8": "^3.2.4",
84
- "nano-staged": "^0.8.0",
85
- "rimraf": "^6.0.1",
86
- "tsx": "^4.20.6",
87
- "typescript": "^5.7.3",
88
- "vitest": "^3.2.4"
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"