rman 1.0.9 → 1.0.12
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 +43 -25
- package/cli.js +2 -0
- package/commands/github-release.command.d.ts +3 -0
- package/commands/github-release.command.js +119 -0
- package/commands/publish.command.js +6 -41
- package/constants.js +1 -1
- package/core/config.d.ts +128 -6
- package/core/config.js +176 -7
- package/core/repository.d.ts +8 -3
- package/core/repository.js +75 -11
- package/interfaces/rman-config.interface.d.ts +98 -30
- package/package.json +1 -1
- package/rmanrc.schema.json +63 -47
- package/services/github-release.service.d.ts +10 -9
- package/services/github-release.service.js +15 -22
- package/services/publish.service.js +113 -47
- package/services/run.service.d.ts +8 -3
- package/services/run.service.js +63 -15
- package/services/version.service.js +67 -3
- package/utils/change-hash.d.ts +2 -2
- package/utils/change-hash.js +2 -2
- package/utils/npm-run-path.d.ts +1 -1
- package/utils/version-stamp.d.ts +40 -0
- package/utils/version-stamp.js +76 -0
package/README.md
CHANGED
|
@@ -265,11 +265,10 @@ algorithm, prerelease semantics, and `"workspace:"` dependency-range handling.
|
|
|
265
265
|
|
|
266
266
|
### `rman publish`
|
|
267
267
|
|
|
268
|
-
Publishes every package to its configured
|
|
269
|
-
own `.rmanrc "publish.target"` says (`"npm"`, `"docker"`,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
tag on the github side.
|
|
268
|
+
Publishes every package to its configured registry - `npm` by default, or whatever each package's
|
|
269
|
+
own `.rmanrc "publish.target"` says (`"npm"`, `"docker"`, or both). Each target decides for itself
|
|
270
|
+
whether the current version is already out there: `npm view` on the npm side, `docker manifest
|
|
271
|
+
inspect` on the docker side.
|
|
273
272
|
|
|
274
273
|
```bash
|
|
275
274
|
rman publish # show the plan, then ask for confirmation
|
|
@@ -281,7 +280,6 @@ rman publish --otp 123456
|
|
|
281
280
|
rman publish --registry https://registry.example.com --userconfig ./ci.npmrc
|
|
282
281
|
rman publish --package-manager pnpm
|
|
283
282
|
rman publish --target docker # only the packages configured for the "docker" target
|
|
284
|
-
rman publish --target github # only the GitHub Release side of it
|
|
285
283
|
```
|
|
286
284
|
|
|
287
285
|
A `"workspace:*"`/`"workspace:^"`/`"workspace:~"` dependency range is automatically rewritten to a
|
|
@@ -292,10 +290,19 @@ A package opts into building/pushing a Docker image via `.rmanrc "publish.target
|
|
|
292
290
|
a `"publish.docker"` block (`image`, `platforms`, `buildContexts`, `buildArgs`, ...) - see
|
|
293
291
|
[docs/cli/publish.md#docker-publishing-publishdocker](docs/cli/publish.md#docker-publishing-publishdocker).
|
|
294
292
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
293
|
+
### `rman github-release`
|
|
294
|
+
|
|
295
|
+
Creates the repository's GitHub Release for the version that just shipped - one per run, named after
|
|
296
|
+
the repository's own release tag, with notes covering every package that shipped under it.
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
rman github-release --yes
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
It is deliberately neither a `publish.target` nor opt-in: a release isn't a registry a package ships
|
|
303
|
+
to, it's the repository's own record that a version shipped, and every repository wants that record.
|
|
304
|
+
It needs no configuration at all - see
|
|
305
|
+
[docs/cli/github-release.md](docs/cli/github-release.md).
|
|
299
306
|
|
|
300
307
|
### `rman import <path>`
|
|
301
308
|
|
|
@@ -320,29 +327,40 @@ precedence: `package.json`'s own `"rman"` key, `.rmanrc.yml` (YAML), `.rmanrc` (
|
|
|
320
327
|
the dotfile-style name), and `.rmanrc.cjs`/`.rmanrc.mjs`/`.rmanrc.js` for config that needs real
|
|
321
328
|
logic (a JS module's default export).
|
|
322
329
|
|
|
330
|
+
**Who a declaration is about** follows one rule: unmarked keys configure the package of the
|
|
331
|
+
directory declaring them, and a `"[selector]"` block configures the packages it names. So the
|
|
332
|
+
repository root's own keys are the *root package's* - which is where repo-wide settings are read
|
|
333
|
+
from anyway - and they reach the other packages only through a selector.
|
|
334
|
+
|
|
323
335
|
```yaml
|
|
324
336
|
# .rmanrc.yml, at the repository root
|
|
325
337
|
packageManager: pnpm
|
|
326
338
|
logLevel: info
|
|
327
339
|
allowBranch: [main, release/*]
|
|
328
340
|
|
|
329
|
-
group: true # implicit repo-wide version group by default
|
|
330
|
-
|
|
331
341
|
version:
|
|
332
342
|
commitMessage: 'chore(release): v{version}'
|
|
333
343
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
344
|
+
'[*]': # every package in the repository - quotes are required in YAML
|
|
345
|
+
group: true # implicit repo-wide version group by default
|
|
346
|
+
changelog:
|
|
347
|
+
ignoreTypes: [chore, ci]
|
|
348
|
+
tagPattern: 'v*'
|
|
349
|
+
clean:
|
|
350
|
+
include: [build, '../../coverage/${{ pkg.basename }}'] # any string may embed a JS expression
|
|
351
|
+
run:
|
|
352
|
+
test: mocha # a bare string is shorthand for { exec: mocha }
|
|
353
|
+
build:
|
|
354
|
+
concurrency: 4
|
|
355
|
+
before: [rman run lint]
|
|
356
|
+
exec: tsc -b tsconfig-build.json
|
|
357
|
+
after: node ../../support/postbuild.cjs
|
|
358
|
+
lint:
|
|
359
|
+
topo: false
|
|
360
|
+
bail: false
|
|
361
|
+
|
|
362
|
+
'[*-dialect]': # a glob over package names, anchored at both ends
|
|
363
|
+
group: dialects
|
|
346
364
|
```
|
|
347
365
|
|
|
348
366
|
```json
|
|
@@ -356,7 +374,7 @@ run:
|
|
|
356
374
|
```
|
|
357
375
|
|
|
358
376
|
See [docs/api.md#configuration-rmanrc-rmanrcyml](docs/api.md#configuration-rmanrc-rmanrcyml) for the
|
|
359
|
-
full key reference (every `run.<script>.*` sub-key, `clean.*`, `changelog.*`, precedence
|
|
377
|
+
full key reference (every `run.<script>.*` sub-key, `clean.*`, `changelog.*`, selector precedence,
|
|
360
378
|
and which keys are root-level-only today).
|
|
361
379
|
|
|
362
380
|
**Editor autocomplete:** `rman` ships a JSON Schema for `.rmanrc`/`.rmanrc.yml` at
|
package/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as ciCommand from './commands/ci.command.js';
|
|
|
11
11
|
import * as cleanCommand from './commands/clean.command.js';
|
|
12
12
|
import * as diffCommand from './commands/diff.command.js';
|
|
13
13
|
import * as execCommand from './commands/exec.command.js';
|
|
14
|
+
import * as githubReleaseCommand from './commands/github-release.command.js';
|
|
14
15
|
import * as importCommand from './commands/import.command.js';
|
|
15
16
|
import * as infoCommand from './commands/info.command.js';
|
|
16
17
|
import * as listCommand from './commands/list.command.js';
|
|
@@ -61,6 +62,7 @@ export async function runCli(options) {
|
|
|
61
62
|
testCommand.initCli(repository, program);
|
|
62
63
|
versionCommand.initCli(repository, program);
|
|
63
64
|
publishCommand.initCli(repository, program);
|
|
65
|
+
githubReleaseCommand.initCli(repository, program);
|
|
64
66
|
execCommand.initCli(repository, program);
|
|
65
67
|
changedCommand.initCli(repository, program);
|
|
66
68
|
diffCommand.initCli(repository, program);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import colors from 'ansi-colors';
|
|
3
|
+
import { GithubReleaseService } from '../services/github-release.service.js';
|
|
4
|
+
import { applyBranchGuardOptions, assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
|
|
5
|
+
export function initCli(repository, program) {
|
|
6
|
+
program.command({
|
|
7
|
+
command: 'github-release',
|
|
8
|
+
describe: "Creates the repository's GitHub Release for the version that just shipped",
|
|
9
|
+
builder: cmd => applyBranchGuardOptions(cmd)
|
|
10
|
+
.example('$0 github-release', '# Show what would be released, then ask for confirmation')
|
|
11
|
+
.example('$0 github-release --yes', '# Create it immediately, no confirmation (CI)')
|
|
12
|
+
.example('$0 github-release --dry-run', '# Only show the plan')
|
|
13
|
+
.option('yes', {
|
|
14
|
+
alias: 'y',
|
|
15
|
+
describe: 'Skip the confirmation prompt and create the release immediately',
|
|
16
|
+
type: 'boolean',
|
|
17
|
+
})
|
|
18
|
+
.option('dry-run', {
|
|
19
|
+
describe: 'Only show the plan - never creates anything, regardless of --yes',
|
|
20
|
+
type: 'boolean',
|
|
21
|
+
})
|
|
22
|
+
.option('json', {
|
|
23
|
+
alias: 'j',
|
|
24
|
+
describe: 'Print the plan as JSON instead of text',
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
})
|
|
27
|
+
.option('repository', {
|
|
28
|
+
describe: 'The "owner/repo" the release is created in - default: .rmanrc "githubRelease.repository", ' +
|
|
29
|
+
'falling back to the "origin" remote.',
|
|
30
|
+
type: 'string',
|
|
31
|
+
})
|
|
32
|
+
.option('ignore-dirty', {
|
|
33
|
+
describe: 'Release anyway when the working tree has uncommitted changes, instead of aborting',
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
}),
|
|
36
|
+
handler: async (args) => {
|
|
37
|
+
await assertAllowedBranch(repository, readBranchGuardOptions(args));
|
|
38
|
+
// No package filtering: a release belongs to the repository, not to a package, so there is
|
|
39
|
+
// nothing for --scope/--ignore to narrow down.
|
|
40
|
+
const plan = await GithubReleaseService.getPlan(repository, {
|
|
41
|
+
ignoreDirty: args.ignoreDirty,
|
|
42
|
+
repository: args.repository,
|
|
43
|
+
});
|
|
44
|
+
if (args.json) {
|
|
45
|
+
console.log(JSON.stringify(plan.map(e => ({
|
|
46
|
+
tag: e.tag,
|
|
47
|
+
repository: e.repository,
|
|
48
|
+
status: e.status,
|
|
49
|
+
version: e.version,
|
|
50
|
+
reason: e.reason,
|
|
51
|
+
})), undefined, 2));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
for (const e of plan) {
|
|
55
|
+
const name = colors.cyan(e.tag ?? e.version);
|
|
56
|
+
switch (e.status) {
|
|
57
|
+
case 'publish':
|
|
58
|
+
console.log(colors.green('release'), name, colors.gray(`${e.repository} - ${e.reason ?? ''}`));
|
|
59
|
+
break;
|
|
60
|
+
case 'up-to-date':
|
|
61
|
+
console.log(colors.gray('up-to-date'), name, colors.gray(e.reason ?? ''));
|
|
62
|
+
break;
|
|
63
|
+
case 'skip':
|
|
64
|
+
console.log(colors.cyan('skip'), name, colors.gray(e.reason ?? ''));
|
|
65
|
+
break;
|
|
66
|
+
case 'error':
|
|
67
|
+
console.log(colors.red('error'), name, colors.red(e.reason ?? ''));
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const error = plan.find(e => e.status === 'error');
|
|
73
|
+
if (error) {
|
|
74
|
+
const err = new Error(error.reason ?? 'Unable to prepare the GitHub Release');
|
|
75
|
+
err.logged = true;
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
if (!plan.some(e => e.status === 'publish')) {
|
|
79
|
+
if (!args.json)
|
|
80
|
+
console.log(colors.gray('Nothing to release.'));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (args.dryRun)
|
|
84
|
+
return;
|
|
85
|
+
let proceed = !!args.yes;
|
|
86
|
+
if (!proceed) {
|
|
87
|
+
if (!process.stdout.isTTY) {
|
|
88
|
+
console.log(colors.gray('Not a TTY - refusing to prompt. Pass --yes to release non-interactively.'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
proceed = await confirm('Create this release?');
|
|
92
|
+
}
|
|
93
|
+
if (!proceed)
|
|
94
|
+
return;
|
|
95
|
+
const applied = await GithubReleaseService.applyPlan(repository, plan);
|
|
96
|
+
const failed = applied.find(e => e.status === 'error');
|
|
97
|
+
if (failed) {
|
|
98
|
+
console.log(colors.red('failed'), colors.cyan(failed.tag ?? ''), colors.red(failed.reason ?? ''));
|
|
99
|
+
const err = new Error('"github-release" failed');
|
|
100
|
+
err.logged = true;
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
for (const e of applied) {
|
|
104
|
+
if (e.status === 'publish')
|
|
105
|
+
console.log(colors.green('released'), colors.cyan(e.tag ?? ''));
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function confirm(question) {
|
|
111
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
112
|
+
try {
|
|
113
|
+
const answer = await rl.question(`${question} (y/N) `);
|
|
114
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
rl.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -2,7 +2,6 @@ import readline from 'node:readline/promises';
|
|
|
2
2
|
import colors from 'ansi-colors';
|
|
3
3
|
import { CiService } from '../services/ci.service.js';
|
|
4
4
|
import { DockerPublishService } from '../services/docker-publish.service.js';
|
|
5
|
-
import { GithubReleaseService } from '../services/github-release.service.js';
|
|
6
5
|
import { PublishService } from '../services/publish.service.js';
|
|
7
6
|
import { applyBranchGuardOptions, assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
|
|
8
7
|
import { applyPackageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
|
|
@@ -15,7 +14,6 @@ export function initCli(repository, program) {
|
|
|
15
14
|
.example('$0 publish --yes', '# Publish immediately, no confirmation')
|
|
16
15
|
.example('$0 publish --dry-run', '# Only show the plan, never publish')
|
|
17
16
|
.example('$0 publish --target docker', '# Only the packages configured for the "docker" target')
|
|
18
|
-
.example('$0 publish --target github', '# Only the GitHub Release side of it')
|
|
19
17
|
.option('yes', {
|
|
20
18
|
alias: 'y',
|
|
21
19
|
describe: 'Skip the confirmation prompt and publish immediately',
|
|
@@ -32,12 +30,12 @@ export function initCli(repository, program) {
|
|
|
32
30
|
type: 'boolean',
|
|
33
31
|
})
|
|
34
32
|
.option('target', {
|
|
35
|
-
describe: 'Restrict this run to just these publish target(s) ("npm"/"docker"
|
|
33
|
+
describe: 'Restrict this run to just these publish target(s) ("npm"/"docker", repeatable) - default: ' +
|
|
36
34
|
'every target each package itself is configured for (.rmanrc "publish.target", "npm" when unset). ' +
|
|
37
35
|
'A package that opts into "docker" but has no "publish.docker" config errors clearly instead of ' +
|
|
38
36
|
'being silently skipped.',
|
|
39
37
|
type: 'array',
|
|
40
|
-
choices: ['npm', 'docker'
|
|
38
|
+
choices: ['npm', 'docker'],
|
|
41
39
|
})
|
|
42
40
|
.option('ignore-dirty', {
|
|
43
41
|
describe: 'Exclude a package with uncommitted local changes instead of aborting the whole run',
|
|
@@ -76,18 +74,12 @@ export function initCli(repository, program) {
|
|
|
76
74
|
describe: 'Prefixed onto a bare (no "/") "publish.docker.image" - default: the DOCKERHUB_NAMESPACE ' +
|
|
77
75
|
'environment variable.',
|
|
78
76
|
type: 'string',
|
|
79
|
-
})
|
|
80
|
-
.option('github-repository', {
|
|
81
|
-
describe: 'The "owner/repo" GitHub Releases are created in - default: each package\'s own ' +
|
|
82
|
-
'"publish.github.repository", falling back to the "origin" remote.',
|
|
83
|
-
type: 'string',
|
|
84
77
|
}),
|
|
85
78
|
handler: async (args) => {
|
|
86
79
|
await assertAllowedBranch(repository, readBranchGuardOptions(args));
|
|
87
80
|
const targets = resolveTargets(args.target);
|
|
88
81
|
const explicitTargets = !!args.target?.length;
|
|
89
82
|
const explicitDockerTarget = explicitTargets && targets.has('docker');
|
|
90
|
-
const explicitGithubTarget = explicitTargets && targets.has('github');
|
|
91
83
|
const ignoreDirty = args.ignoreDirty;
|
|
92
84
|
const npmOptions = {
|
|
93
85
|
...readPackageFilterOptions(args),
|
|
@@ -100,23 +92,14 @@ export function initCli(repository, program) {
|
|
|
100
92
|
ignoreDirty,
|
|
101
93
|
namespace: args.dockerNamespace,
|
|
102
94
|
};
|
|
103
|
-
// No package filtering: a GitHub Release belongs to the repository, not to a package, so
|
|
104
|
-
// there is nothing for --scope/--ignore to narrow down.
|
|
105
|
-
const githubOptions = { ignoreDirty, repository: args.githubRepository };
|
|
106
95
|
const npmPlan = targets.has('npm') ? await PublishService.getPlan(repository, npmOptions) : [];
|
|
107
96
|
const dockerPlan = targets.has('docker') ? await DockerPublishService.getPlan(repository, dockerOptions) : [];
|
|
108
|
-
const githubPlan = targets.has('github') ? await GithubReleaseService.getPlan(repository, githubOptions) : [];
|
|
109
97
|
if (args.json) {
|
|
110
|
-
console.log(JSON.stringify([
|
|
111
|
-
...npmPlan.map(e => jsonEntry(e, 'npm')),
|
|
112
|
-
...dockerPlan.map(e => jsonEntry(e, 'docker')),
|
|
113
|
-
...githubPlan.map(e => jsonEntry(e, 'github')),
|
|
114
|
-
], undefined, 2));
|
|
98
|
+
console.log(JSON.stringify([...npmPlan.map(e => jsonEntry(e, 'npm')), ...dockerPlan.map(e => jsonEntry(e, 'docker'))], undefined, 2));
|
|
115
99
|
}
|
|
116
100
|
else {
|
|
117
101
|
printPlan(npmPlan);
|
|
118
102
|
printPlan(dockerPlan, 'docker');
|
|
119
|
-
printPlan(githubPlan, 'github');
|
|
120
103
|
}
|
|
121
104
|
if (explicitDockerTarget && !dockerPlan.length) {
|
|
122
105
|
const message = '--target docker was given, but no package\'s .rmanrc configures "publish.docker".';
|
|
@@ -125,14 +108,7 @@ export function initCli(repository, program) {
|
|
|
125
108
|
err.logged = true;
|
|
126
109
|
throw err;
|
|
127
110
|
}
|
|
128
|
-
|
|
129
|
-
const message = '--target github was given, but nothing in .rmanrc opts into the "github" target.';
|
|
130
|
-
console.log(colors.red(message));
|
|
131
|
-
const err = new Error(message);
|
|
132
|
-
err.logged = true;
|
|
133
|
-
throw err;
|
|
134
|
-
}
|
|
135
|
-
const errors = [...npmPlan, ...dockerPlan, ...githubPlan].filter(e => e.status === 'error');
|
|
111
|
+
const errors = [...npmPlan, ...dockerPlan].filter(e => e.status === 'error');
|
|
136
112
|
if (errors.length) {
|
|
137
113
|
const allDirty = errors.every(e => e.reason === 'uncommitted local changes');
|
|
138
114
|
const message = allDirty
|
|
@@ -144,7 +120,7 @@ export function initCli(repository, program) {
|
|
|
144
120
|
err.logged = true;
|
|
145
121
|
throw err;
|
|
146
122
|
}
|
|
147
|
-
if (![...npmPlan, ...dockerPlan
|
|
123
|
+
if (![...npmPlan, ...dockerPlan].some(e => e.status === 'publish')) {
|
|
148
124
|
if (!args.json)
|
|
149
125
|
console.log(colors.gray('Nothing to publish.'));
|
|
150
126
|
return;
|
|
@@ -172,7 +148,6 @@ export function initCli(repository, program) {
|
|
|
172
148
|
})
|
|
173
149
|
: [];
|
|
174
150
|
const appliedDocker = targets.has('docker') ? await DockerPublishService.applyPlan(repository, dockerPlan) : [];
|
|
175
|
-
const appliedGithub = targets.has('github') ? await GithubReleaseService.applyPlan(repository, githubPlan) : [];
|
|
176
151
|
let failed = false;
|
|
177
152
|
for (const entry of appliedNpm) {
|
|
178
153
|
if (entry.status === 'publish') {
|
|
@@ -193,16 +168,6 @@ export function initCli(repository, program) {
|
|
|
193
168
|
console.log(colors.red('failed'), colors.gray('[docker]'), colors.cyan(entry.package.name), colors.red(entry.reason ?? ''));
|
|
194
169
|
}
|
|
195
170
|
}
|
|
196
|
-
for (const entry of appliedGithub) {
|
|
197
|
-
if (entry.status === 'publish') {
|
|
198
|
-
console.log(colors.green('released'), colors.gray('[github]'), colors.cyan(entry.tag ?? ''));
|
|
199
|
-
}
|
|
200
|
-
else if (entry.status === 'error' &&
|
|
201
|
-
githubPlan.find(e => e.package === entry.package)?.status === 'publish') {
|
|
202
|
-
failed = true;
|
|
203
|
-
console.log(colors.red('failed'), colors.gray('[github]'), colors.cyan(entry.package.name), colors.red(entry.reason ?? ''));
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
171
|
if (failed) {
|
|
207
172
|
const err = new Error('"publish" failed');
|
|
208
173
|
err.logged = true;
|
|
@@ -213,7 +178,7 @@ export function initCli(repository, program) {
|
|
|
213
178
|
}
|
|
214
179
|
function resolveTargets(input) {
|
|
215
180
|
if (!input?.length)
|
|
216
|
-
return new Set(['npm', 'docker'
|
|
181
|
+
return new Set(['npm', 'docker']);
|
|
217
182
|
return new Set(input);
|
|
218
183
|
}
|
|
219
184
|
/** One `--json` row. `target` is what distinguishes otherwise-identical rows for a package that
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '1.0.
|
|
1
|
+
export const version = '1.0.12';
|
package/core/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
1
2
|
import type { RmanConfig } from '../interfaces/rman-config.interface.js';
|
|
2
3
|
/**
|
|
3
4
|
* Identity helper for authoring a `.rmanrc.cjs`/`.mjs`/`.js` config with full type-checking and
|
|
@@ -21,10 +22,131 @@ export declare function defineConfig(config: RmanConfig): RmanConfig;
|
|
|
21
22
|
*/
|
|
22
23
|
export declare function readDirConfig(dirname: string): Promise<RmanConfig>;
|
|
23
24
|
/**
|
|
24
|
-
* Resolves the effective config for `targetDir
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* Resolves the effective config for the package at `targetDir`, cascading from `rootDir` down to
|
|
26
|
+
* it (inclusive) - each directory level overrides the ones above it, the way tsconfig's `extends`
|
|
27
|
+
* chain does.
|
|
28
|
+
*
|
|
29
|
+
* Every level contributes in two ways, and the difference is the whole model:
|
|
30
|
+
*
|
|
31
|
+
* - **Unmarked keys configure the package of the directory that declares them.** The root's own
|
|
32
|
+
* `.rmanrc` therefore configures the *root package* - which is where repo-wide settings
|
|
33
|
+
* (`packageManager`, `allowBranch`, `version.*`, `githubRelease.*`) are read from anyway - and
|
|
34
|
+
* not, silently, every package under it.
|
|
35
|
+
* - **A `"[selector]"` block configures the packages it names** (`"[*]"` for all of them,
|
|
36
|
+
* `"[*-dialect]"` for a glob over package names). This is the only way a directory speaks about
|
|
37
|
+
* anything but its own package.
|
|
38
|
+
*
|
|
39
|
+
* Splitting the two matters because the same key means different things to the two audiences. The
|
|
40
|
+
* clearest case is `run.<script>.postScript`: on a package it's that package's build hook, run in
|
|
41
|
+
* its own directory; on the root it's a repo-wide bookend run once at the repository root. A
|
|
42
|
+
* cascade that fed one declaration to both ran a package-relative command (`node
|
|
43
|
+
* ../../support/postbuild.cjs`) at the root, where it cannot resolve.
|
|
44
|
+
*
|
|
45
|
+
* `packageName` is what selectors match against; without it (resolving the root's own config, say)
|
|
46
|
+
* selector blocks contribute nothing at all.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveConfig(rootDir: string, targetDir: string, cache?: Map<string, RmanConfig>, packageName?: string): Promise<RmanConfig>;
|
|
49
|
+
/** A config key naming packages rather than settings: `"[*]"`, `"[*-dialect]"`, `"[pkg-a]"`. The
|
|
50
|
+
* brackets are what keep this space from colliding with real config keys - no setting starts with
|
|
51
|
+
* one - and in YAML they also mean the key always needs quoting (`"[*]":`), since a bare `[*]`
|
|
52
|
+
* parses as a flow sequence. */
|
|
53
|
+
export declare function isSelectorKey(key: string): boolean;
|
|
54
|
+
/** The glob inside a selector key, as a `RegExp` anchored at both ends - so `"[*-dialect]"` matches
|
|
55
|
+
* `mysql-dialect` but not `my-dialect-helper`. Glob rather than regex, to match every other
|
|
56
|
+
* pattern in rman (`allowBranch`, `changelog.tagPattern`, `clean.include`). */
|
|
57
|
+
export declare function selectorToRegExp(key: string): RegExp;
|
|
58
|
+
/** One package, as an expression sees it - the same shape for the package the config belongs to
|
|
59
|
+
* and for the repository itself, so `${{ repository.basename }}` reads the way `${{ pkg.basename }}` does. */
|
|
60
|
+
export interface PackageScope {
|
|
61
|
+
/** The package's own name, scope included (`@sqb/builder`). */
|
|
62
|
+
name: string;
|
|
63
|
+
/** Just the scope (`@sqb`), or `undefined` for an unscoped package. */
|
|
64
|
+
scope: string | undefined;
|
|
65
|
+
/** The name with its scope stripped (`builder`). */
|
|
66
|
+
unscopedName: string;
|
|
67
|
+
version: string;
|
|
68
|
+
/** The package directory's last segment (`builder`) - not always the same as `unscopedName`,
|
|
69
|
+
* which is why both exist, and usually what a sibling path (`../../coverage/builder`) is keyed on. */
|
|
70
|
+
basename: string;
|
|
71
|
+
/** Absolute path to the package's own directory - named as rman's own `Package.dirname` is. */
|
|
72
|
+
dirname: string;
|
|
73
|
+
/** That directory relative to the repository root (`packages/builder`), which is what a command
|
|
74
|
+
* addressing another package from the root usually needs. Empty string for the root itself. */
|
|
75
|
+
relativeDir: string;
|
|
76
|
+
/** The whole `package.json`, as a copy - so an expression can reach a field rman itself has no
|
|
77
|
+
* opinion about (`pkg.json.engines.node`). */
|
|
78
|
+
json: Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
/** Facts about the repository, on top of the root package's own - because the repository root *is*
|
|
81
|
+
* a package (`repository.name` is what its `package.json` says, `repository.basename` the directory
|
|
82
|
+
* it sits in, and the two genuinely differ). Sharing `PackageScope`'s shape is what makes
|
|
83
|
+
* `repository.version` read the way `pkg.version` does. */
|
|
84
|
+
export interface RepositoryScope extends PackageScope {
|
|
85
|
+
monorepo: boolean;
|
|
86
|
+
/** Every package in the repository - the root included only when it *is* the one package. */
|
|
87
|
+
packages: PackageScope[];
|
|
88
|
+
/** One package by name, or `undefined` - for reaching a sibling's directory. */
|
|
89
|
+
package(name: string): PackageScope | undefined;
|
|
90
|
+
/** Read from git only if an expression actually asks for it, then remembered: a repository that
|
|
91
|
+
* never mentions these pays nothing, and every command resolves config. All `undefined` outside
|
|
92
|
+
* a git checkout, which is a legitimate state rather than an error. */
|
|
93
|
+
git: GitScope;
|
|
94
|
+
}
|
|
95
|
+
export interface GitScope {
|
|
96
|
+
branch: string | undefined;
|
|
97
|
+
sha: string | undefined;
|
|
98
|
+
shortSha: string | undefined;
|
|
99
|
+
/** Whether the working tree has uncommitted changes. */
|
|
100
|
+
dirty: boolean | undefined;
|
|
101
|
+
}
|
|
102
|
+
/** What a `${{ ... }}` expression can see - the bindings of the fresh global it is evaluated in.
|
|
103
|
+
* Namespaced rather than a flat bag of loose names: one obvious place per fact, and room to add
|
|
104
|
+
* helpers to `pkg`/`repository` later without crowding the global. */
|
|
105
|
+
export interface ConfigScope {
|
|
106
|
+
/** The package the config was resolved for - which is what lets one declaration at the root
|
|
107
|
+
* still say something package-specific. */
|
|
108
|
+
pkg: PackageScope;
|
|
109
|
+
repository: RepositoryScope;
|
|
110
|
+
env: Record<string, string | undefined>;
|
|
111
|
+
/** rman's own `semver`, for the arithmetic every release config eventually wants
|
|
112
|
+
* (`semver.major(pkg.version)`). */
|
|
113
|
+
semver: typeof semver;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Evaluates every `${{ ... }}` expression in **every** string value of a resolved config, against
|
|
117
|
+
* the package it was resolved for:
|
|
118
|
+
*
|
|
119
|
+
* ```yaml
|
|
120
|
+
* "[*]":
|
|
121
|
+
* clean:
|
|
122
|
+
* include: ["build", "../../coverage/${{ pkg.basename }}"]
|
|
123
|
+
* publish:
|
|
124
|
+
* docker:
|
|
125
|
+
* image: "panates/${{ pkg.basename }}:${{ semver.major(pkg.version) }}"
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* Every string, with no list of "interpolated keys" to memorize - a rule with exceptions is a rule
|
|
129
|
+
* nobody remembers.
|
|
130
|
+
*
|
|
131
|
+
* The contents are **real JavaScript**, not a template mini-language, so there is no growing list
|
|
132
|
+
* of substitutions to keep adding (`{{major}}`, `{{scope}}`, ...) - see `ConfigScope` for what is
|
|
133
|
+
* in scope.
|
|
134
|
+
*
|
|
135
|
+
* **`${{ }}`, deliberately not `{{ }}`.** A config value may legitimately carry `{{...}}` meant for
|
|
136
|
+
* something else entirely (`helm template --set tag={{.Values.tag}}`); with the plainer delimiter
|
|
137
|
+
* rman would try to evaluate it. To emit a literal, let an expression produce it, the way GitHub
|
|
138
|
+
* Actions does: `${{ '${{' }}`.
|
|
139
|
+
*
|
|
140
|
+
* A string that is *nothing but* one expression keeps the value's own type (`"${{ pkg.private }}"`
|
|
141
|
+
* -> a boolean), since otherwise this could only ever produce strings and settings like
|
|
142
|
+
* `run.<script>.skip` would be unreachable. Embedded in surrounding text it is stringified.
|
|
143
|
+
*
|
|
144
|
+
* Evaluation happens in a fresh V8 context holding only the scope's bindings. That is a clean
|
|
145
|
+
* scope, **not a sandbox** - `node:vm` is explicitly not a security mechanism, and no sandbox is
|
|
146
|
+
* called for here anyway: a `.rmanrc` that can say `exec: "..."` already runs arbitrary shell, so
|
|
147
|
+
* the expression evaluator adds no trust boundary that wasn't already wide open.
|
|
148
|
+
*
|
|
149
|
+
* A failing expression throws with the config path that holds it, rather than being left in place:
|
|
150
|
+
* silently passing through a mistake is how a config ends up quietly doing nothing.
|
|
29
151
|
*/
|
|
30
|
-
export declare function
|
|
152
|
+
export declare function interpolateConfig<T>(config: T, scope: ConfigScope): T;
|