@oorabona/release-it-preset 1.0.0 → 1.2.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 +15 -10
- package/bin/cli.js +12 -17
- package/bin/validators.js +34 -0
- package/dist/scripts/doctor.js +138 -0
- package/dist/scripts/init-project.js +158 -7
- package/dist/scripts/lib/workspace-detect.js +173 -0
- package/dist/types/doctor.d.ts +6 -0
- package/dist/types/init-project.d.ts +25 -1
- package/dist/types/lib/workspace-detect.d.ts +59 -0
- package/package.json +20 -12
- package/scripts/templates/workflows/release.yml.template +63 -0
package/README.md
CHANGED
|
@@ -575,6 +575,12 @@ pnpm release-it-preset init
|
|
|
575
575
|
|
|
576
576
|
# Non-interactive mode (skip prompts, use defaults)
|
|
577
577
|
pnpm release-it-preset init --yes
|
|
578
|
+
|
|
579
|
+
# Also scaffold a GitHub Actions publish workflow
|
|
580
|
+
pnpm release-it-preset init --yes --with-workflows
|
|
581
|
+
|
|
582
|
+
# Use a custom workflow filename (default: release.yml)
|
|
583
|
+
pnpm release-it-preset init --yes --with-workflows --workflow-name=publish.yml
|
|
578
584
|
```
|
|
579
585
|
|
|
580
586
|
**What it does:**
|
|
@@ -654,7 +660,7 @@ pnpm release-it-preset doctor --json
|
|
|
654
660
|
|----------|--------|
|
|
655
661
|
| Environment | Known env vars, source (env / default / unset), publish-mode consistency |
|
|
656
662
|
| Repository | Git repo presence, branch vs `GIT_REQUIRE_BRANCH`, latest tag, commit count, dirty WD, upstream tracking, remote URL |
|
|
657
|
-
| Configuration | `CHANGELOG.md` exists + Keep a Changelog format + `[Unreleased]` content, `.release-it.json` parseable + `extends` field, `package.json` valid semver version |
|
|
663
|
+
| Configuration | `CHANGELOG.md` exists + Keep a Changelog format + `[Unreleased]` content, `.release-it.json` parseable + `extends` field, `package.json` valid semver version, `release-it` peer range satisfied, `release-it` major version advisor |
|
|
658
664
|
| Readiness Summary | `PASS`/`WARN`/`FAIL` counts, score `N/M checks passing`, status (`READY`/`WARNINGS`/`BLOCKED`), actionable recommendations |
|
|
659
665
|
|
|
660
666
|
**Exit codes:**
|
|
@@ -896,7 +902,7 @@ pnpm release-it-preset default
|
|
|
896
902
|
- The `extends` field loads the preset
|
|
897
903
|
- release-it merges your overrides on top via c12
|
|
898
904
|
- **Your values take precedence** over preset defaults
|
|
899
|
-
- CLI validates that `extends`
|
|
905
|
+
- CLI validates that `extends` is present; mismatched preset name warns and uses the invoked preset's config for that run
|
|
900
906
|
|
|
901
907
|
**Pros:**
|
|
902
908
|
- ✅ **Recommended for customization**
|
|
@@ -951,7 +957,7 @@ pnpm release-it-preset default
|
|
|
951
957
|
|
|
952
958
|
**Why `extends` is required:** Without it, release-it only loads your config file and uses release-it's own defaults. The preset is never loaded, so you lose important defaults like `npm.publish: false`.
|
|
953
959
|
|
|
954
|
-
####
|
|
960
|
+
#### Note: Preset mismatch (warning, not error)
|
|
955
961
|
|
|
956
962
|
```bash
|
|
957
963
|
# .release-it.json extends "default":
|
|
@@ -962,15 +968,14 @@ pnpm release-it-preset default
|
|
|
962
968
|
# But you run:
|
|
963
969
|
pnpm release-it-preset hotfix
|
|
964
970
|
|
|
965
|
-
#
|
|
966
|
-
#
|
|
967
|
-
# .release-it.json
|
|
968
|
-
#
|
|
969
|
-
# Either:
|
|
970
|
-
# 1. Run: release-it-preset default
|
|
971
|
-
# 2. Update .release-it.json extends to: "@oorabona/release-it-preset/config/hotfix"
|
|
971
|
+
# ⚠️ Note: your .release-it.json extends "default"
|
|
972
|
+
# but you invoked the "hotfix" preset.
|
|
973
|
+
# Using "hotfix" config directly; .release-it.json customizations are ignored for this run.
|
|
974
|
+
# To use your customizations, run: release-it-preset default
|
|
972
975
|
```
|
|
973
976
|
|
|
977
|
+
> **Note**: invoking a preset different from your `.release-it.json` extends value now warns and uses the invoked preset's config (was: hard error). Use the matching name to keep your customizations.
|
|
978
|
+
|
|
974
979
|
---
|
|
975
980
|
|
|
976
981
|
### Which Mode Should I Use?
|
package/bin/cli.js
CHANGED
|
@@ -160,21 +160,19 @@ function handleReleaseCommand(configName, args) {
|
|
|
160
160
|
const extendsPreset = extendsMatch?.[1];
|
|
161
161
|
|
|
162
162
|
if (extendsPreset && extendsPreset !== configName) {
|
|
163
|
-
console.
|
|
164
|
-
console.
|
|
165
|
-
console.
|
|
166
|
-
console.
|
|
167
|
-
console.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
console.
|
|
172
|
-
console.
|
|
173
|
-
|
|
163
|
+
console.warn(`⚠️ Note: your .release-it.json extends "${extendsPreset}"`);
|
|
164
|
+
console.warn(` but you invoked the "${configName}" preset.`);
|
|
165
|
+
console.warn(` Using "${configName}" config directly; .release-it.json customizations are ignored for this run.`);
|
|
166
|
+
console.warn(` To use your customizations, run: release-it-preset ${extendsPreset}`);
|
|
167
|
+
console.warn(``);
|
|
168
|
+
// Force --config flag to use OUR preset, ignore user's .release-it.json
|
|
169
|
+
fullArgs = ['--config', configPath, ...args];
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`✅ Config validated: preset "${configName}"`);
|
|
172
|
+
console.log(`📝 Using: ${userConfigPath}\n`);
|
|
173
|
+
// Let release-it discover .release-it.json and merge via extends
|
|
174
|
+
fullArgs = [...args];
|
|
174
175
|
}
|
|
175
|
-
|
|
176
|
-
console.log(`✅ Config validated: preset "${configName}"`);
|
|
177
|
-
console.log(`📝 Using: ${userConfigPath}\n`);
|
|
178
176
|
} catch (error) {
|
|
179
177
|
if (error instanceof SyntaxError) {
|
|
180
178
|
console.error(`❌ Failed to parse .release-it.json: ${error.message}`);
|
|
@@ -183,9 +181,6 @@ function handleReleaseCommand(configName, args) {
|
|
|
183
181
|
}
|
|
184
182
|
process.exit(1);
|
|
185
183
|
}
|
|
186
|
-
|
|
187
|
-
// Let release-it discover .release-it.json and merge via extends
|
|
188
|
-
fullArgs = [...args];
|
|
189
184
|
} else {
|
|
190
185
|
// No user config - use preset directly
|
|
191
186
|
console.log(`📝 Using preset config directly: ${configPath}`);
|
package/bin/validators.js
CHANGED
|
@@ -117,6 +117,40 @@ export function sanitizeArgs(args) {
|
|
|
117
117
|
* @throws {Error} If validation fails (invalid extension, too deep, missing file, etc.)
|
|
118
118
|
* @returns {string} Absolute path to validated config file
|
|
119
119
|
*/
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validates a workflow filename for use with `init --workflow-name`.
|
|
123
|
+
*
|
|
124
|
+
* Allowed: simple filenames matching ^[A-Za-z0-9._-]+\.ya?ml$
|
|
125
|
+
* Rejected: path components (subdir/), traversal (../), wrong extension.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} name - The workflow filename to validate
|
|
128
|
+
* @throws {Error} If the name contains path separators, traversal, or wrong extension
|
|
129
|
+
* @returns {string} The validated filename
|
|
130
|
+
*/
|
|
131
|
+
export function validateWorkflowName(name) {
|
|
132
|
+
if (!name || name.length === 0) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Workflow name cannot be empty.\n` +
|
|
135
|
+
`Expected a simple filename like "release.yml" or "publish.yml".`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Reject any path separators or traversal — name must be a single filename component
|
|
140
|
+
const WORKFLOW_NAME_RE = /^[A-Za-z0-9._-]+\.ya?ml$/;
|
|
141
|
+
if (!WORKFLOW_NAME_RE.test(name)) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Invalid workflow name: "${name}"\n` +
|
|
144
|
+
`Workflow name must be a simple filename matching ^[A-Za-z0-9._-]+\\.ya?ml$\n` +
|
|
145
|
+
`Examples: release.yml, publish.yml, release_it.yml\n` +
|
|
146
|
+
`Path components (subdir/foo.yml) and traversal (../etc.yml) are not allowed.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return name;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
120
154
|
export function validateConfigPath(configPath) {
|
|
121
155
|
// 1. Whitelist config file extensions (defense in depth)
|
|
122
156
|
const allowedExtensions = ['.json', '.js', '.cjs', '.mjs', '.yaml', '.yml', '.toml'];
|
package/dist/scripts/doctor.js
CHANGED
|
@@ -396,11 +396,149 @@ export function validateConfiguration(deps) {
|
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
398
|
checks.push(detectWorkspaceIntegration(deps));
|
|
399
|
+
for (const result of validateReleaseItPeer(deps)) {
|
|
400
|
+
checks.push(result);
|
|
401
|
+
}
|
|
399
402
|
return { checks, status: worstStatus(checks.map((c) => c.status)) };
|
|
400
403
|
}
|
|
401
404
|
// ---------------------------------------------------------------------------
|
|
402
405
|
// 4. Summary
|
|
403
406
|
// ---------------------------------------------------------------------------
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
// 3b. Release-it peer-dependency checks
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
/**
|
|
411
|
+
* Reads the preset's declared peerDependencies.release-it range.
|
|
412
|
+
* Looks in node_modules/@oorabona/release-it-preset/package.json first
|
|
413
|
+
* (installed package context), then ./package.json (source repo / dev),
|
|
414
|
+
* then falls back to a hardcoded constant.
|
|
415
|
+
*/
|
|
416
|
+
function readPresetPeerRange(deps) {
|
|
417
|
+
const FALLBACK = '^19.0.0 || ^20.0.0';
|
|
418
|
+
const candidates = [
|
|
419
|
+
'node_modules/@oorabona/release-it-preset/package.json',
|
|
420
|
+
'package.json',
|
|
421
|
+
];
|
|
422
|
+
for (const candidate of candidates) {
|
|
423
|
+
try {
|
|
424
|
+
if (!deps.existsSync(candidate))
|
|
425
|
+
continue;
|
|
426
|
+
const raw = deps.readFileSync(candidate, 'utf8');
|
|
427
|
+
const pkg = JSON.parse(raw);
|
|
428
|
+
const peers = pkg.peerDependencies;
|
|
429
|
+
if (peers?.['release-it']) {
|
|
430
|
+
return peers['release-it'];
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// continue to next candidate
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return FALLBACK;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Extracts the highest major version number from a semver range string.
|
|
441
|
+
* Handles OR-joined ranges like "^19.0.0 || ^20.0.0" → 20.
|
|
442
|
+
*/
|
|
443
|
+
function highestMajorFromRange(range) {
|
|
444
|
+
const matches = range.match(/(\d+)\.\d+\.\d+/g) ?? [];
|
|
445
|
+
let max = 0;
|
|
446
|
+
for (const m of matches) {
|
|
447
|
+
const major = parseInt(m.split('.')[0], 10);
|
|
448
|
+
if (major > max)
|
|
449
|
+
max = major;
|
|
450
|
+
}
|
|
451
|
+
return max;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Checks whether an installed version satisfies a simplified peer range.
|
|
455
|
+
* Supports "^X.Y.Z || ^A.B.C" — checks that the installed major matches
|
|
456
|
+
* any major present in the range.
|
|
457
|
+
*/
|
|
458
|
+
function satisfiesPeerRange(version, range) {
|
|
459
|
+
const installedMajor = parseInt(version.replace(/^v/, '').split('.')[0], 10);
|
|
460
|
+
const allowedMajors = Array.from(range.matchAll(/[~^]?(\d+)\.\d+\.\d+/g), (m) => parseInt(m[1], 10));
|
|
461
|
+
return allowedMajors.includes(installedMajor);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Runs Check A (peer range satisfaction) and Check B (major version advisor).
|
|
465
|
+
* Returns an array of CheckResult to be appended into validateConfiguration.
|
|
466
|
+
* Check B is silently skipped when the npm registry is unreachable.
|
|
467
|
+
*/
|
|
468
|
+
export function validateReleaseItPeer(deps) {
|
|
469
|
+
const results = [];
|
|
470
|
+
const peerRange = readPresetPeerRange(deps);
|
|
471
|
+
// --- Check A: release-it in supported peer range ---
|
|
472
|
+
const lsOutput = safeExec('npm ls release-it --depth=0 --json', deps);
|
|
473
|
+
if (!lsOutput) {
|
|
474
|
+
results.push({
|
|
475
|
+
name: 'release-it peer dependency',
|
|
476
|
+
status: 'FAIL',
|
|
477
|
+
value: 'not found',
|
|
478
|
+
detail: 'release-it is not installed. Run: pnpm add -D release-it@^20',
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
let installedVersion;
|
|
483
|
+
try {
|
|
484
|
+
const parsed = JSON.parse(lsOutput);
|
|
485
|
+
installedVersion = parsed.dependencies?.['release-it']?.version;
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// parse failure treated as not found
|
|
489
|
+
}
|
|
490
|
+
if (!installedVersion) {
|
|
491
|
+
results.push({
|
|
492
|
+
name: 'release-it peer dependency',
|
|
493
|
+
status: 'FAIL',
|
|
494
|
+
value: 'not found',
|
|
495
|
+
detail: 'release-it is not installed. Run: pnpm add -D release-it@^20',
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
else if (!satisfiesPeerRange(installedVersion, peerRange)) {
|
|
499
|
+
results.push({
|
|
500
|
+
name: 'release-it peer dependency',
|
|
501
|
+
status: 'FAIL',
|
|
502
|
+
value: installedVersion,
|
|
503
|
+
detail: `Installed release-it ${installedVersion} is outside the supported range (${peerRange}). Run: pnpm add -D release-it@^20`,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
results.push({
|
|
508
|
+
name: 'release-it peer dependency',
|
|
509
|
+
status: 'PASS',
|
|
510
|
+
value: installedVersion,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// --- Check B: release-it major version advisor ---
|
|
515
|
+
// On network failure (null), skip the check entirely — no FAIL on outage.
|
|
516
|
+
const latestOutput = safeExec('npm view release-it version', deps);
|
|
517
|
+
if (latestOutput) {
|
|
518
|
+
const latestVersion = latestOutput.trim();
|
|
519
|
+
const latestMajor = parseInt(latestVersion.replace(/^v/, '').split('.')[0], 10);
|
|
520
|
+
const supportedMaxMajor = highestMajorFromRange(peerRange);
|
|
521
|
+
if (!Number.isNaN(latestMajor) && !Number.isNaN(supportedMaxMajor)) {
|
|
522
|
+
if (latestMajor > supportedMaxMajor) {
|
|
523
|
+
results.push({
|
|
524
|
+
name: 'release-it major version',
|
|
525
|
+
status: 'WARN',
|
|
526
|
+
value: latestVersion,
|
|
527
|
+
detail: `release-it ${latestMajor}.x available; preset's peer range max is ${supportedMaxMajor}.x. Coordinate with the preset maintainer before upgrading.`,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
results.push({
|
|
532
|
+
name: 'release-it major version',
|
|
533
|
+
status: 'PASS',
|
|
534
|
+
value: latestVersion,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// If latestOutput is null (network failure), push nothing for Check B.
|
|
540
|
+
return results;
|
|
541
|
+
}
|
|
404
542
|
export function summarize(report) {
|
|
405
543
|
const allChecks = [
|
|
406
544
|
...report.environment.checks,
|
|
@@ -13,9 +13,27 @@
|
|
|
13
13
|
* Options:
|
|
14
14
|
* --yes Skip prompts and use defaults
|
|
15
15
|
*/
|
|
16
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
17
|
import { createInterface } from 'node:readline';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
18
20
|
import { runScript } from './lib/run-script.js';
|
|
21
|
+
import { parsePnpmWorkspaceYaml, parseWorkspacesFromPackageJson, resolvePackagePaths, } from './lib/workspace-detect.js';
|
|
22
|
+
import { ValidationError } from './lib/errors.js';
|
|
23
|
+
// Single source of truth for workflow name validation within this file.
|
|
24
|
+
// NOTE: bin/validators.js cannot be imported here — TypeScript compiles scripts/ to
|
|
25
|
+
// dist/scripts/ so a relative '../bin/' import resolves to 'dist/bin/' at runtime
|
|
26
|
+
// (wrong). The canonical regex lives in bin/validators.js:validateWorkflowName and
|
|
27
|
+
// is kept in sync here manually (same pattern: ^[A-Za-z0-9._-]+\.ya?ml$).
|
|
28
|
+
const WORKFLOW_NAME_REGEX = /^[A-Za-z0-9._-]+\.ya?ml$/;
|
|
29
|
+
function assertValidWorkflowName(name) {
|
|
30
|
+
if (!WORKFLOW_NAME_REGEX.test(name)) {
|
|
31
|
+
throw new ValidationError(`Invalid workflow name: "${name}"\n` +
|
|
32
|
+
`Workflow name must match [A-Za-z0-9._-]+\\.ya?ml (no path components, no traversal).\n` +
|
|
33
|
+
`Examples: release.yml, publish.yml\n` +
|
|
34
|
+
`Path components and traversal (../etc.yml) are not allowed.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
19
37
|
const CHANGELOG_TEMPLATE = `# Changelog
|
|
20
38
|
|
|
21
39
|
All notable changes to this project will be documented in this file.
|
|
@@ -34,6 +52,9 @@ const RELEASE_IT_CONFIG = `{
|
|
|
34
52
|
`;
|
|
35
53
|
const SUGGESTED_SCRIPTS = {
|
|
36
54
|
'release': 'release-it-preset default',
|
|
55
|
+
'release:patch': 'release-it-preset default patch',
|
|
56
|
+
'release:minor': 'release-it-preset default minor',
|
|
57
|
+
'release:major': 'release-it-preset default major',
|
|
37
58
|
'release:hotfix': 'release-it-preset hotfix',
|
|
38
59
|
'release:dry': 'release-it-preset default --dry-run',
|
|
39
60
|
'changelog:update': 'release-it-preset update',
|
|
@@ -41,8 +62,13 @@ const SUGGESTED_SCRIPTS = {
|
|
|
41
62
|
export function parseArgs(args) {
|
|
42
63
|
/* c8 ignore next */
|
|
43
64
|
const argv = args || process.argv.slice(2);
|
|
65
|
+
// Extract --workflow-name=<value>
|
|
66
|
+
const workflowNameArg = argv.find(a => a.startsWith('--workflow-name='));
|
|
67
|
+
const workflowName = workflowNameArg ? workflowNameArg.slice('--workflow-name='.length) : 'release.yml';
|
|
44
68
|
return {
|
|
45
69
|
yes: argv.includes('--yes') || argv.includes('-y'),
|
|
70
|
+
withWorkflows: argv.includes('--with-workflows'),
|
|
71
|
+
workflowName,
|
|
46
72
|
};
|
|
47
73
|
}
|
|
48
74
|
export async function createChangelog(options, deps) {
|
|
@@ -132,27 +158,144 @@ export async function updatePackageJson(options, deps) {
|
|
|
132
158
|
return false;
|
|
133
159
|
}
|
|
134
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Write the GitHub Actions workflow file to .github/workflows/<name>.
|
|
163
|
+
* Skips silently if the file already exists (existing skip-on-conflict policy).
|
|
164
|
+
*/
|
|
165
|
+
export async function writeWorkflow(options, deps) {
|
|
166
|
+
// Defense-in-depth: validate workflow name before any path computation.
|
|
167
|
+
// The CLI entry point also validates, but programmatic callers (tests, library use) bypass it.
|
|
168
|
+
assertValidWorkflowName(options.workflowName);
|
|
169
|
+
const workflowDir = join('.github', 'workflows');
|
|
170
|
+
const workflowPath = join(workflowDir, options.workflowName);
|
|
171
|
+
if (deps.existsSync(workflowPath)) {
|
|
172
|
+
deps.log(`ℹ️ ${workflowPath} already exists — skipping.`);
|
|
173
|
+
deps.log(` To integrate manually, review the template and merge into your existing workflow.`);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
// Resolve template path: try compiled position first, fall back to source position.
|
|
177
|
+
// compiled: dist/scripts/init-project.js → ../../scripts/templates/... (2 hops up)
|
|
178
|
+
// source: scripts/init-project.ts → ./templates/... (sibling dir)
|
|
179
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
180
|
+
const __dirname = dirname(__filename);
|
|
181
|
+
const compiledPath = join(__dirname, '..', '..', 'scripts', 'templates', 'workflows', 'release.yml.template');
|
|
182
|
+
const sourcePath = join(__dirname, 'templates', 'workflows', 'release.yml.template');
|
|
183
|
+
const templatePath = deps.existsSync(compiledPath) ? compiledPath : sourcePath;
|
|
184
|
+
let templateContent;
|
|
185
|
+
try {
|
|
186
|
+
templateContent = deps.readFileSync(templatePath, 'utf8');
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw new ValidationError(`Failed to read workflow template at "${templatePath}".\n` +
|
|
190
|
+
`The template ships in scripts/templates/ — if it is missing, reinstall the package.\n` +
|
|
191
|
+
`Original error: ${error}`);
|
|
192
|
+
}
|
|
193
|
+
// Ensure .github/workflows/ exists
|
|
194
|
+
if (!deps.existsSync(workflowDir)) {
|
|
195
|
+
deps.mkdirSync(workflowDir, { recursive: true });
|
|
196
|
+
}
|
|
197
|
+
deps.writeFileSync(workflowPath, templateContent);
|
|
198
|
+
deps.log(`✅ Created ${workflowPath}`);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Detect workspaces from pnpm-workspace.yaml or package.json#workspaces.
|
|
203
|
+
* Returns resolved absolute package directory paths.
|
|
204
|
+
* Returns empty array if no workspace config found.
|
|
205
|
+
* Throws ValidationError if workspace patterns escape the project root.
|
|
206
|
+
*/
|
|
207
|
+
export function detectWorkspaces(projectRoot, deps) {
|
|
208
|
+
const pnpmWorkspaceFile = join(projectRoot, 'pnpm-workspace.yaml');
|
|
209
|
+
const packageJsonFile = join(projectRoot, 'package.json');
|
|
210
|
+
let patterns = [];
|
|
211
|
+
let workspaceConfigExists = false;
|
|
212
|
+
if (deps.existsSync(pnpmWorkspaceFile)) {
|
|
213
|
+
workspaceConfigExists = true;
|
|
214
|
+
const content = deps.readFileSync(pnpmWorkspaceFile, 'utf8');
|
|
215
|
+
patterns = parsePnpmWorkspaceYaml(content);
|
|
216
|
+
}
|
|
217
|
+
else if (deps.existsSync(packageJsonFile)) {
|
|
218
|
+
const content = deps.readFileSync(packageJsonFile, 'utf8');
|
|
219
|
+
patterns = parseWorkspacesFromPackageJson(content);
|
|
220
|
+
if (patterns.length > 0) {
|
|
221
|
+
workspaceConfigExists = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (patterns.length === 0) {
|
|
225
|
+
if (workspaceConfigExists) {
|
|
226
|
+
deps.warn(`ℹ️ Workspace config file found but no packages declared/resolved — ` +
|
|
227
|
+
`treating as single-package init.`);
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
return resolvePackagePaths(patterns, projectRoot, deps);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Scaffold per-package .release-it.json for each detected workspace package.
|
|
235
|
+
* Skips packages that already have .release-it.json (skip-on-conflict policy).
|
|
236
|
+
* Does NOT write a root .release-it.json (would conflict with per-package configs).
|
|
237
|
+
*/
|
|
238
|
+
export async function scaffoldWorkspacePackages(packageDirs, deps) {
|
|
239
|
+
let created = 0;
|
|
240
|
+
for (const pkgDir of packageDirs) {
|
|
241
|
+
const configPath = join(pkgDir, '.release-it.json');
|
|
242
|
+
if (deps.existsSync(configPath)) {
|
|
243
|
+
deps.log(`ℹ️ ${configPath} already exists — skipping`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
deps.writeFileSync(configPath, RELEASE_IT_CONFIG);
|
|
247
|
+
deps.log(`✅ Created ${configPath}`);
|
|
248
|
+
created++;
|
|
249
|
+
}
|
|
250
|
+
return created;
|
|
251
|
+
}
|
|
135
252
|
export async function initProject(options, deps) {
|
|
136
253
|
deps.log('🚀 Initializing project with release-it-preset\n');
|
|
137
254
|
if (options.yes) {
|
|
138
255
|
deps.log('ℹ️ Running in --yes mode (non-interactive)\n');
|
|
139
256
|
}
|
|
257
|
+
// Detect monorepo workspaces before deciding what to scaffold
|
|
258
|
+
const projectRoot = process.cwd();
|
|
259
|
+
const workspaceDirs = detectWorkspaces(projectRoot, deps);
|
|
260
|
+
const isMonorepo = workspaceDirs.length > 0;
|
|
140
261
|
const results = {
|
|
141
262
|
changelog: await createChangelog(options, deps),
|
|
142
|
-
|
|
143
|
-
|
|
263
|
+
// In monorepo mode: per-package configs, NO root .release-it.json
|
|
264
|
+
releaseIt: isMonorepo ? false : await createReleaseItConfig(options, deps),
|
|
265
|
+
packageJson: isMonorepo ? false : await updatePackageJson(options, deps),
|
|
266
|
+
workflow: options.withWorkflows ? await writeWorkflow(options, deps) : false,
|
|
267
|
+
monorepoPackages: isMonorepo ? await scaffoldWorkspacePackages(workspaceDirs, deps) : 0,
|
|
144
268
|
};
|
|
145
269
|
deps.log('\n📊 Summary:');
|
|
146
270
|
deps.log(` CHANGELOG.md: ${results.changelog ? '✅ Created' : '⏭️ Skipped'}`);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
271
|
+
if (isMonorepo) {
|
|
272
|
+
deps.log(` workspace packages: ${results.monorepoPackages} .release-it.json created`);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
deps.log(` .release-it.json: ${results.releaseIt ? '✅ Created' : '⏭️ Skipped'}`);
|
|
276
|
+
deps.log(` package.json: ${results.packageJson ? '✅ Updated' : '⏭️ Skipped'}`);
|
|
277
|
+
}
|
|
278
|
+
if (options.withWorkflows) {
|
|
279
|
+
deps.log(` workflow: ${results.workflow ? '✅ Created' : '⏭️ Skipped'}`);
|
|
280
|
+
}
|
|
281
|
+
const anyCreated = results.changelog ||
|
|
282
|
+
results.releaseIt ||
|
|
283
|
+
results.packageJson ||
|
|
284
|
+
results.workflow ||
|
|
285
|
+
results.monorepoPackages > 0;
|
|
150
286
|
if (anyCreated) {
|
|
151
287
|
deps.log('\n🎉 Initialization complete!');
|
|
152
288
|
deps.log('\nNext steps:');
|
|
153
289
|
deps.log(' 1. Review the generated files');
|
|
154
290
|
deps.log(' 2. Update CHANGELOG.md [Unreleased] section');
|
|
155
|
-
|
|
291
|
+
if (isMonorepo) {
|
|
292
|
+
deps.log(' 3. Release a package: pnpm -F <package-name> exec release-it-preset default --dry-run');
|
|
293
|
+
deps.log(' (use `pnpm -F <package-name> exec release-it-preset default` to release)');
|
|
294
|
+
deps.log(' Note: per-package CHANGELOG.md is not auto-created — set CHANGELOG_FILE=../CHANGELOG.md per package, or run `release-it-preset update` from the root and copy entries manually.');
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
deps.log(' 3. Run: pnpm release-it-preset default --dry-run');
|
|
298
|
+
}
|
|
156
299
|
}
|
|
157
300
|
else {
|
|
158
301
|
deps.log('\n✨ All files already exist, nothing to do!');
|
|
@@ -178,10 +321,18 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
178
321
|
});
|
|
179
322
|
}
|
|
180
323
|
const options = parseArgs();
|
|
324
|
+
// Validate workflow name whenever explicitly provided (even without --with-workflows).
|
|
325
|
+
const argv = process.argv.slice(2);
|
|
326
|
+
const workflowNameExplicit = argv.some(a => a.startsWith('--workflow-name='));
|
|
327
|
+
if (workflowNameExplicit || options.withWorkflows) {
|
|
328
|
+
assertValidWorkflowName(options.workflowName);
|
|
329
|
+
}
|
|
181
330
|
await initProject(options, {
|
|
182
331
|
existsSync,
|
|
183
332
|
readFileSync,
|
|
184
333
|
writeFileSync,
|
|
334
|
+
mkdirSync,
|
|
335
|
+
readdirSync,
|
|
185
336
|
prompt: realPrompt,
|
|
186
337
|
log: console.log,
|
|
187
338
|
warn: console.warn,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace detection utilities for pnpm-workspace.yaml and package.json#workspaces.
|
|
3
|
+
*
|
|
4
|
+
* Scope: block-list `packages:` in pnpm-workspace.yaml only.
|
|
5
|
+
* Flow-style arrays ( packages: [...] ) are rejected with a clear error.
|
|
6
|
+
* Block-list of strings supported. Flow-style arrays AND alias references (`*alias`) are
|
|
7
|
+
* rejected with `ValidationError`. Anchors (`&name`) on entries are silently treated as
|
|
8
|
+
* opaque pattern strings.
|
|
9
|
+
*
|
|
10
|
+
* All functions are pure (no direct FS access) — dependencies are injected for testability.
|
|
11
|
+
*/
|
|
12
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
13
|
+
import { ValidationError } from './errors.js';
|
|
14
|
+
/**
|
|
15
|
+
* Parse the `packages:` block-list section of a pnpm-workspace.yaml file.
|
|
16
|
+
*
|
|
17
|
+
* Only block-sequence form is supported:
|
|
18
|
+
* packages:
|
|
19
|
+
* - 'packages/*'
|
|
20
|
+
* - 'apps/*'
|
|
21
|
+
*
|
|
22
|
+
* Flow-style arrays ( packages: ['a', 'b'] ) are rejected with a ValidationError
|
|
23
|
+
* pointing users toward the block-list form.
|
|
24
|
+
*
|
|
25
|
+
* @param content - Raw YAML file content
|
|
26
|
+
* @returns Array of glob patterns from the packages: block-list, or empty array if no packages: key found
|
|
27
|
+
* @throws ValidationError if flow-style array syntax is detected
|
|
28
|
+
*/
|
|
29
|
+
export function parsePnpmWorkspaceYaml(content) {
|
|
30
|
+
const lines = content.split('\n');
|
|
31
|
+
// Find the `packages:` key
|
|
32
|
+
const packagesLineIdx = lines.findIndex(l => /^packages\s*:/.test(l));
|
|
33
|
+
if (packagesLineIdx === -1) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
// Check for unsupported syntax on the same line
|
|
37
|
+
const packagesLine = lines[packagesLineIdx];
|
|
38
|
+
// Flow-style arrays: packages: ['a', 'b'] or packages: [a, b]
|
|
39
|
+
if (/^packages\s*:\s*\[/.test(packagesLine)) {
|
|
40
|
+
throw new ValidationError(`pnpm-workspace.yaml uses flow-style array for "packages:" which is not supported.\n` +
|
|
41
|
+
`Please convert to block-list form:\n` +
|
|
42
|
+
` packages:\n` +
|
|
43
|
+
` - 'packages/*'\n` +
|
|
44
|
+
` - 'apps/*'\n` +
|
|
45
|
+
`If you need flow-style support, open an issue at https://github.com/oorabona/release-it-preset/issues`);
|
|
46
|
+
}
|
|
47
|
+
// YAML alias reference: packages: *anchorName
|
|
48
|
+
if (/^packages\s*:\s*\*\S+/.test(packagesLine)) {
|
|
49
|
+
throw new ValidationError(`pnpm-workspace.yaml uses a YAML alias reference for "packages:" which is not supported.\n` +
|
|
50
|
+
`Please convert to block-list form:\n` +
|
|
51
|
+
` packages:\n` +
|
|
52
|
+
` - 'packages/*'\n` +
|
|
53
|
+
` - 'apps/*'\n` +
|
|
54
|
+
`Anchor/alias support is not planned; if you need it, open an issue at https://github.com/oorabona/release-it-preset/issues`);
|
|
55
|
+
}
|
|
56
|
+
// Collect subsequent indented list items (- '...' or - "..." or - bare)
|
|
57
|
+
const patterns = [];
|
|
58
|
+
for (let i = packagesLineIdx + 1; i < lines.length; i++) {
|
|
59
|
+
const line = lines[i];
|
|
60
|
+
// Stop if we hit a non-indented, non-empty line (new top-level key)
|
|
61
|
+
if (line.length > 0 && !/^\s/.test(line)) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
// Match a block-sequence item
|
|
65
|
+
const match = line.match(/^\s+-\s+['"]?([^'"#\n]+?)['"]?\s*(?:#.*)?$/);
|
|
66
|
+
if (match) {
|
|
67
|
+
patterns.push(match[1].trim());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return patterns;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse the `workspaces` field from a package.json content string.
|
|
74
|
+
*
|
|
75
|
+
* Supports:
|
|
76
|
+
* "workspaces": ["packages/*"] — array form
|
|
77
|
+
* "workspaces": {"packages": [...]} — object form (Yarn-style)
|
|
78
|
+
*
|
|
79
|
+
* @param content - Raw package.json content
|
|
80
|
+
* @returns Array of glob patterns, or empty array if workspaces not present
|
|
81
|
+
*/
|
|
82
|
+
export function parseWorkspacesFromPackageJson(content) {
|
|
83
|
+
let pkg;
|
|
84
|
+
try {
|
|
85
|
+
pkg = JSON.parse(content);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
if (typeof pkg !== 'object' || pkg === null || !('workspaces' in pkg)) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const ws = pkg.workspaces;
|
|
94
|
+
if (Array.isArray(ws)) {
|
|
95
|
+
return ws.filter((v) => typeof v === 'string');
|
|
96
|
+
}
|
|
97
|
+
if (typeof ws === 'object' && ws !== null && 'packages' in ws) {
|
|
98
|
+
const pkgs = ws.packages;
|
|
99
|
+
if (Array.isArray(pkgs)) {
|
|
100
|
+
return pkgs.filter((v) => typeof v === 'string');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Expand glob patterns to resolved package directories that contain a package.json.
|
|
107
|
+
*
|
|
108
|
+
* Only supports single-level glob expansion: `packages/*` expands to immediate
|
|
109
|
+
* children of `packages/`. Nested globs (`packages/*\/src`) are not supported
|
|
110
|
+
* and are returned as-is only if the literal path has a package.json.
|
|
111
|
+
*
|
|
112
|
+
* Each resolved path is validated to be contained within projectRoot using
|
|
113
|
+
* path.resolve + path.relative containment check. Paths escaping the root
|
|
114
|
+
* (e.g. `../etc`) throw a ValidationError.
|
|
115
|
+
*
|
|
116
|
+
* @param patterns - Glob patterns from workspace config
|
|
117
|
+
* @param projectRoot - Absolute path to the project root
|
|
118
|
+
* @param deps - Injected FS dependencies
|
|
119
|
+
* @returns Array of absolute paths to valid package directories
|
|
120
|
+
*/
|
|
121
|
+
export function resolvePackagePaths(patterns, projectRoot, deps) {
|
|
122
|
+
const result = [];
|
|
123
|
+
for (const pattern of patterns) {
|
|
124
|
+
const resolved = resolve(projectRoot, pattern);
|
|
125
|
+
// Containment check: relative path must NOT start with '..' or be absolute
|
|
126
|
+
// (isAbsolute guard handles Windows cross-drive paths where relative() returns
|
|
127
|
+
// an absolute path that doesn't start with '..', bypassing the startsWith check)
|
|
128
|
+
const rel = relative(projectRoot, resolved);
|
|
129
|
+
if (isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`)) {
|
|
130
|
+
throw new ValidationError(`Workspace pattern "${pattern}" resolves outside the project root.\n` +
|
|
131
|
+
`Resolved: ${resolved}\n` +
|
|
132
|
+
`Project root: ${projectRoot}\n` +
|
|
133
|
+
`Each workspace package must live under the project root.`);
|
|
134
|
+
}
|
|
135
|
+
// Single-level glob expansion: path ends with /*
|
|
136
|
+
if (pattern.endsWith('/*')) {
|
|
137
|
+
const parentDir = resolve(projectRoot, pattern.slice(0, -2));
|
|
138
|
+
// Containment check on the parent dir too (same isAbsolute guard)
|
|
139
|
+
const parentRel = relative(projectRoot, parentDir);
|
|
140
|
+
if (isAbsolute(parentRel) || parentRel === '..' || parentRel.startsWith(`..${sep}`)) {
|
|
141
|
+
throw new ValidationError(`Workspace pattern "${pattern}" resolves outside the project root.\n` +
|
|
142
|
+
`Resolved parent: ${parentDir}\n` +
|
|
143
|
+
`Project root: ${projectRoot}\n` +
|
|
144
|
+
`Each workspace package must live under the project root.`);
|
|
145
|
+
}
|
|
146
|
+
if (!deps.existsSync(parentDir)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
let entries;
|
|
150
|
+
try {
|
|
151
|
+
entries = deps.readdirSync(parentDir);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const pkgDir = join(parentDir, entry);
|
|
158
|
+
const pkgJson = join(pkgDir, 'package.json');
|
|
159
|
+
if (deps.existsSync(pkgJson)) {
|
|
160
|
+
result.push(pkgDir);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Non-glob: treat as literal directory path
|
|
166
|
+
const pkgJson = join(resolved, 'package.json');
|
|
167
|
+
if (deps.existsSync(pkgJson)) {
|
|
168
|
+
result.push(resolved);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
package/dist/types/doctor.d.ts
CHANGED
|
@@ -65,6 +65,12 @@ export declare function safeExec(command: string, deps: DoctorDeps): string | nu
|
|
|
65
65
|
export declare function collectEnvironment(deps: DoctorDeps): EnvironmentSection;
|
|
66
66
|
export declare function inspectRepository(deps: DoctorDeps): RepositorySection;
|
|
67
67
|
export declare function validateConfiguration(deps: DoctorDeps): ConfigurationSection;
|
|
68
|
+
/**
|
|
69
|
+
* Runs Check A (peer range satisfaction) and Check B (major version advisor).
|
|
70
|
+
* Returns an array of CheckResult to be appended into validateConfiguration.
|
|
71
|
+
* Check B is silently skipped when the npm registry is unreachable.
|
|
72
|
+
*/
|
|
73
|
+
export declare function validateReleaseItPeer(deps: DoctorDeps): CheckResult[];
|
|
68
74
|
export declare function summarize(report: Omit<DoctorReport, 'summary'>): DoctorSummary;
|
|
69
75
|
export declare function runDoctor(deps: DoctorDeps): DoctorReport;
|
|
70
76
|
export declare function formatHuman(report: DoctorReport): string;
|
|
@@ -13,14 +13,18 @@
|
|
|
13
13
|
* Options:
|
|
14
14
|
* --yes Skip prompts and use defaults
|
|
15
15
|
*/
|
|
16
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
17
|
interface Options {
|
|
18
18
|
yes: boolean;
|
|
19
|
+
withWorkflows: boolean;
|
|
20
|
+
workflowName: string;
|
|
19
21
|
}
|
|
20
22
|
export interface InitProjectDeps {
|
|
21
23
|
existsSync: typeof existsSync;
|
|
22
24
|
readFileSync: typeof readFileSync;
|
|
23
25
|
writeFileSync: typeof writeFileSync;
|
|
26
|
+
mkdirSync: typeof mkdirSync;
|
|
27
|
+
readdirSync: typeof readdirSync;
|
|
24
28
|
prompt: (question: string) => Promise<boolean>;
|
|
25
29
|
log: (message: string) => void;
|
|
26
30
|
warn: (message: string) => void;
|
|
@@ -29,9 +33,29 @@ export declare function parseArgs(args?: string[]): Options;
|
|
|
29
33
|
export declare function createChangelog(options: Options, deps: InitProjectDeps): Promise<boolean>;
|
|
30
34
|
export declare function createReleaseItConfig(options: Options, deps: InitProjectDeps): Promise<boolean>;
|
|
31
35
|
export declare function updatePackageJson(options: Options, deps: InitProjectDeps): Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* Write the GitHub Actions workflow file to .github/workflows/<name>.
|
|
38
|
+
* Skips silently if the file already exists (existing skip-on-conflict policy).
|
|
39
|
+
*/
|
|
40
|
+
export declare function writeWorkflow(options: Options, deps: InitProjectDeps): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Detect workspaces from pnpm-workspace.yaml or package.json#workspaces.
|
|
43
|
+
* Returns resolved absolute package directory paths.
|
|
44
|
+
* Returns empty array if no workspace config found.
|
|
45
|
+
* Throws ValidationError if workspace patterns escape the project root.
|
|
46
|
+
*/
|
|
47
|
+
export declare function detectWorkspaces(projectRoot: string, deps: InitProjectDeps): string[];
|
|
48
|
+
/**
|
|
49
|
+
* Scaffold per-package .release-it.json for each detected workspace package.
|
|
50
|
+
* Skips packages that already have .release-it.json (skip-on-conflict policy).
|
|
51
|
+
* Does NOT write a root .release-it.json (would conflict with per-package configs).
|
|
52
|
+
*/
|
|
53
|
+
export declare function scaffoldWorkspacePackages(packageDirs: string[], deps: InitProjectDeps): Promise<number>;
|
|
32
54
|
export declare function initProject(options: Options, deps: InitProjectDeps): Promise<{
|
|
33
55
|
changelog: boolean;
|
|
34
56
|
releaseIt: boolean;
|
|
35
57
|
packageJson: boolean;
|
|
58
|
+
workflow: boolean;
|
|
59
|
+
monorepoPackages: number;
|
|
36
60
|
}>;
|
|
37
61
|
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace detection utilities for pnpm-workspace.yaml and package.json#workspaces.
|
|
3
|
+
*
|
|
4
|
+
* Scope: block-list `packages:` in pnpm-workspace.yaml only.
|
|
5
|
+
* Flow-style arrays ( packages: [...] ) are rejected with a clear error.
|
|
6
|
+
* Block-list of strings supported. Flow-style arrays AND alias references (`*alias`) are
|
|
7
|
+
* rejected with `ValidationError`. Anchors (`&name`) on entries are silently treated as
|
|
8
|
+
* opaque pattern strings.
|
|
9
|
+
*
|
|
10
|
+
* All functions are pure (no direct FS access) — dependencies are injected for testability.
|
|
11
|
+
*/
|
|
12
|
+
export interface WorkspaceDetectDeps {
|
|
13
|
+
existsSync: (path: string) => boolean;
|
|
14
|
+
readdirSync: (path: string) => string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse the `packages:` block-list section of a pnpm-workspace.yaml file.
|
|
18
|
+
*
|
|
19
|
+
* Only block-sequence form is supported:
|
|
20
|
+
* packages:
|
|
21
|
+
* - 'packages/*'
|
|
22
|
+
* - 'apps/*'
|
|
23
|
+
*
|
|
24
|
+
* Flow-style arrays ( packages: ['a', 'b'] ) are rejected with a ValidationError
|
|
25
|
+
* pointing users toward the block-list form.
|
|
26
|
+
*
|
|
27
|
+
* @param content - Raw YAML file content
|
|
28
|
+
* @returns Array of glob patterns from the packages: block-list, or empty array if no packages: key found
|
|
29
|
+
* @throws ValidationError if flow-style array syntax is detected
|
|
30
|
+
*/
|
|
31
|
+
export declare function parsePnpmWorkspaceYaml(content: string): string[];
|
|
32
|
+
/**
|
|
33
|
+
* Parse the `workspaces` field from a package.json content string.
|
|
34
|
+
*
|
|
35
|
+
* Supports:
|
|
36
|
+
* "workspaces": ["packages/*"] — array form
|
|
37
|
+
* "workspaces": {"packages": [...]} — object form (Yarn-style)
|
|
38
|
+
*
|
|
39
|
+
* @param content - Raw package.json content
|
|
40
|
+
* @returns Array of glob patterns, or empty array if workspaces not present
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseWorkspacesFromPackageJson(content: string): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Expand glob patterns to resolved package directories that contain a package.json.
|
|
45
|
+
*
|
|
46
|
+
* Only supports single-level glob expansion: `packages/*` expands to immediate
|
|
47
|
+
* children of `packages/`. Nested globs (`packages/*\/src`) are not supported
|
|
48
|
+
* and are returned as-is only if the literal path has a package.json.
|
|
49
|
+
*
|
|
50
|
+
* Each resolved path is validated to be contained within projectRoot using
|
|
51
|
+
* path.resolve + path.relative containment check. Paths escaping the root
|
|
52
|
+
* (e.g. `../etc`) throw a ValidationError.
|
|
53
|
+
*
|
|
54
|
+
* @param patterns - Glob patterns from workspace config
|
|
55
|
+
* @param projectRoot - Absolute path to the project root
|
|
56
|
+
* @param deps - Injected FS dependencies
|
|
57
|
+
* @returns Array of absolute paths to valid package directories
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolvePackagePaths(patterns: string[], projectRoot: string, deps: WorkspaceDetectDeps): string[];
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oorabona/release-it-preset",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Release tooling for solo and small-team JS maintainers: human-curated changelogs, OIDC zero-config publishing, recovery presets, monorepo support, pre-release diagnostics.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"release-it",
|
|
8
8
|
"release",
|
|
9
|
-
"version",
|
|
10
9
|
"changelog",
|
|
11
10
|
"conventional-commits",
|
|
12
11
|
"keep-a-changelog",
|
|
@@ -16,9 +15,12 @@
|
|
|
16
15
|
"oidc",
|
|
17
16
|
"trusted-publishing",
|
|
18
17
|
"github-actions",
|
|
19
|
-
"automation",
|
|
20
18
|
"typescript",
|
|
21
|
-
"semver"
|
|
19
|
+
"semver",
|
|
20
|
+
"cli",
|
|
21
|
+
"developer-tools",
|
|
22
|
+
"pnpm",
|
|
23
|
+
"slsa"
|
|
22
24
|
],
|
|
23
25
|
"author": "Olivier Orabona",
|
|
24
26
|
"license": "MIT",
|
|
@@ -51,6 +53,7 @@
|
|
|
51
53
|
"config",
|
|
52
54
|
"dist/scripts",
|
|
53
55
|
"dist/types",
|
|
56
|
+
"scripts/templates",
|
|
54
57
|
"README.md",
|
|
55
58
|
"LICENSE"
|
|
56
59
|
],
|
|
@@ -59,13 +62,13 @@
|
|
|
59
62
|
"release": "pnpm run release:default",
|
|
60
63
|
"release:default": "node ./bin/cli.js default",
|
|
61
64
|
"release:default:dry-run": "node ./bin/cli.js default --dry-run",
|
|
62
|
-
"release:no-changelog": "node ./bin/cli.js no-changelog",
|
|
63
|
-
"release:changelog-only": "node ./bin/cli.js changelog-only",
|
|
64
|
-
"release:manual-changelog": "node ./bin/cli.js manual-changelog",
|
|
65
|
+
"release:dev:no-changelog": "node ./bin/cli.js no-changelog",
|
|
66
|
+
"release:dev:changelog-only": "node ./bin/cli.js changelog-only",
|
|
67
|
+
"release:dev:manual-changelog": "node ./bin/cli.js manual-changelog",
|
|
65
68
|
"release:hotfix": "node ./bin/cli.js hotfix",
|
|
66
|
-
"release:republish": "node ./bin/cli.js republish",
|
|
67
|
-
"release:retry-preflight": "node ./bin/cli.js retry-publish-preflight",
|
|
68
|
-
"release:retry-publish": "node ./bin/cli.js retry-publish",
|
|
69
|
+
"release:dev:republish": "node ./bin/cli.js republish",
|
|
70
|
+
"release:dev:retry-preflight": "node ./bin/cli.js retry-publish-preflight",
|
|
71
|
+
"release:dev:retry-publish": "node ./bin/cli.js retry-publish",
|
|
69
72
|
"release:update": "node ./bin/cli.js update",
|
|
70
73
|
"release:validate": "node ./bin/cli.js validate",
|
|
71
74
|
"release:validate:allow-dirty": "node ./bin/cli.js validate --allow-dirty",
|
|
@@ -82,7 +85,12 @@
|
|
|
82
85
|
"test:watch": "vitest",
|
|
83
86
|
"test:coverage": "vitest run --coverage",
|
|
84
87
|
"test:ui": "vitest --ui",
|
|
85
|
-
"prepublishOnly": "pnpm build && echo 'Running prepublish checks...' && test -f README.md && test -f LICENSE"
|
|
88
|
+
"prepublishOnly": "pnpm build && echo 'Running prepublish checks...' && test -f README.md && test -f LICENSE",
|
|
89
|
+
"release:patch": "node ./bin/cli.js default patch",
|
|
90
|
+
"release:minor": "node ./bin/cli.js default minor",
|
|
91
|
+
"release:major": "node ./bin/cli.js default major",
|
|
92
|
+
"release:dry": "node ./bin/cli.js default --dry-run",
|
|
93
|
+
"changelog:update": "node ./bin/cli.js update"
|
|
86
94
|
},
|
|
87
95
|
"peerDependencies": {
|
|
88
96
|
"release-it": "^19.0.0 || ^20.0.0"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# GitHub Actions workflow for npm + GitHub release publishing.
|
|
2
|
+
# Generated by: release-it-preset init --with-workflows
|
|
3
|
+
#
|
|
4
|
+
# PREREQUISITE: Configure OIDC trusted publishing for this workflow file at
|
|
5
|
+
# https://www.npmjs.com/settings/<your-org>/packages — select "GitHub Actions"
|
|
6
|
+
# and set the workflow path to match this file's location in the repository.
|
|
7
|
+
# Without OIDC enrollment the publish step will fail with a 401 on first tag push.
|
|
8
|
+
# Troubleshooting: https://docs.npmjs.com/trusted-publishers
|
|
9
|
+
|
|
10
|
+
name: Publish Package
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
tags:
|
|
15
|
+
- 'v*'
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
inputs:
|
|
18
|
+
tag:
|
|
19
|
+
description: 'Tag to publish (e.g. v1.2.3). Required when triggered from a branch.'
|
|
20
|
+
type: string
|
|
21
|
+
required: false
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
contents: write
|
|
25
|
+
id-token: write # Required for npm OIDC trusted publishing
|
|
26
|
+
|
|
27
|
+
env:
|
|
28
|
+
NODE_VERSION: '24' # npm >= 11.5.1 ships with Node 24, required for OIDC trusted publishing
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
publish:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- name: Checkout code
|
|
35
|
+
uses: actions/checkout@v6
|
|
36
|
+
with:
|
|
37
|
+
fetch-depth: 0
|
|
38
|
+
ref: ${{ inputs.tag || github.ref }}
|
|
39
|
+
fetch-tags: true
|
|
40
|
+
|
|
41
|
+
- name: Setup pnpm
|
|
42
|
+
uses: pnpm/action-setup@v6
|
|
43
|
+
|
|
44
|
+
- name: Setup Node.js
|
|
45
|
+
uses: actions/setup-node@v6
|
|
46
|
+
with:
|
|
47
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
48
|
+
cache: 'pnpm'
|
|
49
|
+
registry-url: 'https://registry.npmjs.org'
|
|
50
|
+
|
|
51
|
+
- name: Install dependencies
|
|
52
|
+
run: pnpm install --frozen-lockfile
|
|
53
|
+
|
|
54
|
+
- name: Run pre-flight checks
|
|
55
|
+
run: pnpm exec release-it-preset retry-publish-preflight
|
|
56
|
+
|
|
57
|
+
- name: Publish (npm + GitHub release)
|
|
58
|
+
env:
|
|
59
|
+
NPM_PUBLISH: 'true'
|
|
60
|
+
GITHUB_RELEASE: 'true'
|
|
61
|
+
NPM_SKIP_CHECKS: 'true'
|
|
62
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
63
|
+
run: pnpm exec release-it-preset retry-publish --ci
|