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 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 target(s) - `npm` by default, or whatever each package's
269
- own `.rmanrc "publish.target"` says (`"npm"`, `"docker"`, `"github"`, or any combination). Each
270
- target decides for itself whether the current version is already out there: `npm view` on the npm
271
- side, `docker manifest inspect` on the docker side, and the GitHub Release for that version's own
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
- A package with no package registry of its own - a standalone app shipped as release assets, or one
296
- deployed elsewhere with the release just recording that it shipped - opts into
297
- `"publish.target": ["github"]` instead, optionally with `"publish.github": { "assets": [...] }` -
298
- see [docs/cli/publish.md#github-releases-publishgithub](docs/cli/publish.md#github-releases-publishgithub).
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
- changelog:
335
- ignoreTypes: [chore, ci]
336
- tagPattern: 'v*'
337
-
338
- run:
339
- build:
340
- concurrency: 4
341
- lint:
342
- topo: false
343
- bail: false
344
- test:
345
- changedSince: v1.0.0
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 rules,
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,3 @@
1
+ import type { Argv } from 'yargs';
2
+ import type { Repository } from '../core/repository.js';
3
+ export declare function initCli(repository: Repository, program: Argv): void;
@@ -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"/"github", repeatable) - default: ' +
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', 'github'],
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
- if (explicitGithubTarget && !githubPlan.length) {
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, ...githubPlan].some(e => e.status === 'publish')) {
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', 'github']);
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.9';
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` by cascading from `rootDir`
25
- * down to `targetDir` (inclusive), the same way tsconfig's `extends` chain
26
- * works: each directory level overrides the ones above it. This lets a
27
- * package (or any intermediate directory) narrow or override the repository's
28
- * root configuration for itself and everything below it.
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 resolveConfig(rootDir: string, targetDir: string, cache?: Map<string, RmanConfig>): Promise<RmanConfig>;
152
+ export declare function interpolateConfig<T>(config: T, scope: ConfigScope): T;