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/core/config.js CHANGED
@@ -3,7 +3,9 @@ import * as yaml from 'js-yaml';
3
3
  import { createRequire } from 'module';
4
4
  import path from 'path';
5
5
  import merge from 'putil-merge';
6
+ import semver from 'semver';
6
7
  import { pathToFileURL } from 'url';
8
+ import vm from 'vm';
7
9
  /**
8
10
  * Identity helper for authoring a `.rmanrc.cjs`/`.mjs`/`.js` config with full type-checking and
9
11
  * autocomplete - the same `defineConfig` pattern Vite/Vitest use. Returns `config` completely
@@ -85,24 +87,93 @@ export async function readDirConfig(dirname) {
85
87
  return result;
86
88
  }
87
89
  /**
88
- * Resolves the effective config for `targetDir` by cascading from `rootDir`
89
- * down to `targetDir` (inclusive), the same way tsconfig's `extends` chain
90
- * works: each directory level overrides the ones above it. This lets a
91
- * package (or any intermediate directory) narrow or override the repository's
92
- * root configuration for itself and everything below it.
90
+ * Resolves the effective config for the package at `targetDir`, cascading from `rootDir` down to
91
+ * it (inclusive) - each directory level overrides the ones above it, the way tsconfig's `extends`
92
+ * chain does.
93
+ *
94
+ * Every level contributes in two ways, and the difference is the whole model:
95
+ *
96
+ * - **Unmarked keys configure the package of the directory that declares them.** The root's own
97
+ * `.rmanrc` therefore configures the *root package* - which is where repo-wide settings
98
+ * (`packageManager`, `allowBranch`, `version.*`, `githubRelease.*`) are read from anyway - and
99
+ * not, silently, every package under it.
100
+ * - **A `"[selector]"` block configures the packages it names** (`"[*]"` for all of them,
101
+ * `"[*-dialect]"` for a glob over package names). This is the only way a directory speaks about
102
+ * anything but its own package.
103
+ *
104
+ * Splitting the two matters because the same key means different things to the two audiences. The
105
+ * clearest case is `run.<script>.postScript`: on a package it's that package's build hook, run in
106
+ * its own directory; on the root it's a repo-wide bookend run once at the repository root. A
107
+ * cascade that fed one declaration to both ran a package-relative command (`node
108
+ * ../../support/postbuild.cjs`) at the root, where it cannot resolve.
109
+ *
110
+ * `packageName` is what selectors match against; without it (resolving the root's own config, say)
111
+ * selector blocks contribute nothing at all.
93
112
  */
94
- export async function resolveConfig(rootDir, targetDir, cache = new Map()) {
113
+ export async function resolveConfig(rootDir, targetDir, cache = new Map(), packageName) {
95
114
  const result = {};
115
+ const target = path.resolve(targetDir);
96
116
  for (const dir of dirChain(rootDir, targetDir)) {
97
117
  let local = cache.get(dir);
98
118
  if (!local) {
99
119
  local = await readDirConfig(dir);
100
120
  cache.set(dir, local);
101
121
  }
102
- merge(result, local, { deep: true });
122
+ // Selectors first, so a directory's own unmarked config still wins over a selector declared
123
+ // alongside it - "this package" is a more specific statement than "packages matching a glob".
124
+ if (packageName) {
125
+ for (const block of matchingSelectors(local, packageName))
126
+ merge(result, block, { deep: true });
127
+ }
128
+ // A directory holding a package speaks for that package only - which is what keeps the root's
129
+ // own config off every package under it. A directory that holds none (an intermediate
130
+ // `packages/`, say) has no package to speak for, so its unmarked config can only mean
131
+ // "everything below" and still cascades.
132
+ const ownsAPackage = fs.existsSync(path.join(dir, 'package.json'));
133
+ if (!ownsAPackage || path.resolve(dir) === target)
134
+ merge(result, stripSelectors(local), { deep: true });
135
+ }
136
+ return result;
137
+ }
138
+ /** A config key naming packages rather than settings: `"[*]"`, `"[*-dialect]"`, `"[pkg-a]"`. The
139
+ * brackets are what keep this space from colliding with real config keys - no setting starts with
140
+ * one - and in YAML they also mean the key always needs quoting (`"[*]":`), since a bare `[*]`
141
+ * parses as a flow sequence. */
142
+ export function isSelectorKey(key) {
143
+ return key.length > 2 && key.startsWith('[') && key.endsWith(']');
144
+ }
145
+ /** The glob inside a selector key, as a `RegExp` anchored at both ends - so `"[*-dialect]"` matches
146
+ * `mysql-dialect` but not `my-dialect-helper`. Glob rather than regex, to match every other
147
+ * pattern in rman (`allowBranch`, `changelog.tagPattern`, `clean.include`). */
148
+ export function selectorToRegExp(key) {
149
+ const glob = key.slice(1, -1);
150
+ const source = glob
151
+ .split('*')
152
+ .map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
153
+ .join('.*');
154
+ return new RegExp(`^${source}$`);
155
+ }
156
+ /** Every selector block in `config` matching `packageName`, in increasing precedence: `"[*]"` first
157
+ * and the rest in declaration order - so a specific glob overrides the catch-all, and two equally
158
+ * specific ones resolve by the order they were written in. */
159
+ function matchingSelectors(config, packageName) {
160
+ const matches = [];
161
+ for (const [key, value] of Object.entries(config)) {
162
+ if (!isSelectorKey(key) || !value || typeof value !== 'object')
163
+ continue;
164
+ if (selectorToRegExp(key).test(packageName))
165
+ matches.push([key, value]);
103
166
  }
167
+ return matches.sort((a, b) => Number(b[0] === CATCH_ALL) - Number(a[0] === CATCH_ALL)).map(([, block]) => block);
168
+ }
169
+ function stripSelectors(config) {
170
+ const result = {};
171
+ for (const [key, value] of Object.entries(config))
172
+ if (!isSelectorKey(key))
173
+ result[key] = value;
104
174
  return result;
105
175
  }
176
+ const CATCH_ALL = '[*]';
106
177
  function dirChain(rootDir, targetDir) {
107
178
  const rel = path.relative(rootDir, targetDir);
108
179
  if (!rel || rel === '.' || rel.startsWith('..'))
@@ -115,3 +186,101 @@ function dirChain(rootDir, targetDir) {
115
186
  }
116
187
  return dirs;
117
188
  }
189
+ /**
190
+ * Evaluates every `${{ ... }}` expression in **every** string value of a resolved config, against
191
+ * the package it was resolved for:
192
+ *
193
+ * ```yaml
194
+ * "[*]":
195
+ * clean:
196
+ * include: ["build", "../../coverage/${{ pkg.basename }}"]
197
+ * publish:
198
+ * docker:
199
+ * image: "panates/${{ pkg.basename }}:${{ semver.major(pkg.version) }}"
200
+ * ```
201
+ *
202
+ * Every string, with no list of "interpolated keys" to memorize - a rule with exceptions is a rule
203
+ * nobody remembers.
204
+ *
205
+ * The contents are **real JavaScript**, not a template mini-language, so there is no growing list
206
+ * of substitutions to keep adding (`{{major}}`, `{{scope}}`, ...) - see `ConfigScope` for what is
207
+ * in scope.
208
+ *
209
+ * **`${{ }}`, deliberately not `{{ }}`.** A config value may legitimately carry `{{...}}` meant for
210
+ * something else entirely (`helm template --set tag={{.Values.tag}}`); with the plainer delimiter
211
+ * rman would try to evaluate it. To emit a literal, let an expression produce it, the way GitHub
212
+ * Actions does: `${{ '${{' }}`.
213
+ *
214
+ * A string that is *nothing but* one expression keeps the value's own type (`"${{ pkg.private }}"`
215
+ * -> a boolean), since otherwise this could only ever produce strings and settings like
216
+ * `run.<script>.skip` would be unreachable. Embedded in surrounding text it is stringified.
217
+ *
218
+ * Evaluation happens in a fresh V8 context holding only the scope's bindings. That is a clean
219
+ * scope, **not a sandbox** - `node:vm` is explicitly not a security mechanism, and no sandbox is
220
+ * called for here anyway: a `.rmanrc` that can say `exec: "..."` already runs arbitrary shell, so
221
+ * the expression evaluator adds no trust boundary that wasn't already wide open.
222
+ *
223
+ * A failing expression throws with the config path that holds it, rather than being left in place:
224
+ * silently passing through a mistake is how a config ends up quietly doing nothing.
225
+ */
226
+ export function interpolateConfig(config, scope) {
227
+ const context = vm.createContext({ ...scope });
228
+ return walk(config, scope, context, []);
229
+ }
230
+ const EXPRESSION = /\$\{\{([\s\S]*?)\}\}/g;
231
+ function walk(value, scope, context, at) {
232
+ if (typeof value === 'string')
233
+ return interpolateString(value, context, at);
234
+ if (Array.isArray(value))
235
+ return value.map((item, i) => walk(item, scope, context, [...at, i]));
236
+ if (value && typeof value === 'object') {
237
+ const result = {};
238
+ for (const [key, item] of Object.entries(value))
239
+ result[key] = walk(item, scope, context, [...at, key]);
240
+ return result;
241
+ }
242
+ return value;
243
+ }
244
+ function interpolateString(value, context, at) {
245
+ if (!value.includes('${{'))
246
+ return value;
247
+ const found = [...value.matchAll(EXPRESSION)];
248
+ if (!found.length)
249
+ return value;
250
+ /** Counted rather than matched with an anchored `^...$` regex: a lazy quantifier still backtracks
251
+ * to satisfy an end anchor, so `"${{ a }} and ${{ b }}"` looked like *one* expression whose body
252
+ * ran from `a` to `b`, brace-ends and all - invalid JavaScript. */
253
+ const soleExpression = found.length === 1 && found[0][0] === value.trim();
254
+ // Alone, a nullish result is just "this setting is unset" - a legitimate answer.
255
+ if (soleExpression)
256
+ return evaluate(found[0][1], value, context, at);
257
+ return value.replace(EXPRESSION, (_, expr) => {
258
+ const result = evaluate(expr, value, context, at);
259
+ /** Embedded in text, though, it never is: splicing in the word "undefined" produces a path or
260
+ * tag like `app:undefined` that looks plausible and is wrong - the exact silent-mistake shape
261
+ * this evaluator exists to avoid. `?? 'fallback'` says what was meant. */
262
+ if (result === undefined || result === null) {
263
+ const where = at.length ? formatPath(at) : 'the config root';
264
+ throw new Error(`Expression in "${where}" is ${result} inside a string: ${value.trim()}\n` +
265
+ ` \${{${expr}}} has no value here - give it a fallback (\${{${expr.trim()} ?? '...'}}).`);
266
+ }
267
+ return String(result);
268
+ });
269
+ }
270
+ /** Names the config path as well as the expression: an error saying only "x is not defined" sends
271
+ * the reader hunting through a file that may hold dozens of them. */
272
+ function evaluate(expr, source, context, at) {
273
+ try {
274
+ return vm.runInContext(expr, context, { timeout: EXPRESSION_TIMEOUT });
275
+ }
276
+ catch (e) {
277
+ const where = at.length ? formatPath(at) : 'the config root';
278
+ throw new Error(`Invalid expression in "${where}": ${source.trim()}\n ${e?.message ?? e}`, { cause: e });
279
+ }
280
+ }
281
+ function formatPath(at) {
282
+ return at.reduce((acc, part) => (typeof part === 'number' ? `${acc}[${part}]` : acc ? `${acc}.${part}` : String(part)), '');
283
+ }
284
+ /** Guards against an expression that never returns (`while(true)`) taking the whole command with
285
+ * it - a typo, not an attack, but the failure mode is identical. */
286
+ const EXPRESSION_TIMEOUT = 1000;
@@ -37,9 +37,14 @@ export declare class Repository extends Package {
37
37
  hash?: string;
38
38
  }): Promise<Record<string, Repository.PackageStatus>>;
39
39
  /**
40
- * Resolves the effective rman config for the repository root and every
41
- * package, cascading root -> intermediate directories -> package directory,
42
- * so a `.rmanrc` placed anywhere along that path overrides the levels above it.
40
+ * Resolves the effective rman config for the repository root and every package, cascading
41
+ * root -> intermediate directories -> package directory, so a `.rmanrc` placed anywhere along
42
+ * that path overrides the levels above it.
43
+ *
44
+ * Each package is resolved *by name* as well as by directory, since that is what a `"[selector]"`
45
+ * block matches against - see `resolveConfig`. The root package is resolved by name too: in a
46
+ * single-package repository it *is* the one package, so `"[*]"` has to reach it; in a monorepo
47
+ * nothing under `getPackages()` is the root, so only its own unmarked config applies.
43
48
  */
44
49
  protected _resolveConfigs(): Promise<void>;
45
50
  protected _updateDependencies(): void;
@@ -1,8 +1,10 @@
1
+ import { execFileSync } from 'node:child_process';
1
2
  import glob from 'fast-glob';
2
3
  import fs from 'fs';
3
4
  import path from 'path';
5
+ import semver from 'semver';
4
6
  import { GitHelper } from '../utils/git.js';
5
- import { resolveConfig } from './config.js';
7
+ import { interpolateConfig, resolveConfig } from './config.js';
6
8
  import { Package } from './package.js';
7
9
  export class Repository extends Package {
8
10
  dirname;
@@ -87,20 +89,55 @@ export class Repository extends Package {
87
89
  return result;
88
90
  }
89
91
  /**
90
- * Resolves the effective rman config for the repository root and every
91
- * package, cascading root -> intermediate directories -> package directory,
92
- * so a `.rmanrc` placed anywhere along that path overrides the levels above it.
92
+ * Resolves the effective rman config for the repository root and every package, cascading
93
+ * root -> intermediate directories -> package directory, so a `.rmanrc` placed anywhere along
94
+ * that path overrides the levels above it.
95
+ *
96
+ * Each package is resolved *by name* as well as by directory, since that is what a `"[selector]"`
97
+ * block matches against - see `resolveConfig`. The root package is resolved by name too: in a
98
+ * single-package repository it *is* the one package, so `"[*]"` has to reach it; in a monorepo
99
+ * nothing under `getPackages()` is the root, so only its own unmarked config applies.
93
100
  */
94
101
  async _resolveConfigs() {
95
102
  const cache = new Map();
96
- const rootConfig = await resolveConfig(this.dirname, this.dirname, cache);
97
- this.config = rootConfig;
98
- this.rootPackage.config = rootConfig;
103
+ const repoDir = this.dirname;
104
+ const scopeOf = (pkg) => {
105
+ // A package.json without a "name" is unusual but legal, and `info` prints such a package
106
+ // rather than refusing it - so the scope has to survive one too.
107
+ const name = pkg.name ?? '';
108
+ const at = name.lastIndexOf('/');
109
+ return {
110
+ name,
111
+ scope: at > 0 ? name.slice(0, at) : undefined,
112
+ unscopedName: at > 0 ? name.slice(at + 1) : name,
113
+ version: pkg.version ?? '',
114
+ basename: path.basename(pkg.dirname),
115
+ dirname: pkg.dirname,
116
+ relativeDir: path.relative(this.dirname, pkg.dirname),
117
+ // A copy: an expression has no business mutating the package rman is about to act on.
118
+ json: { ...pkg.json },
119
+ };
120
+ };
121
+ const packageScopes = this.packages.map(scopeOf);
122
+ let gitScope;
123
+ const repository = {
124
+ ...scopeOf(this.rootPackage),
125
+ monorepo: this.monorepo,
126
+ packages: packageScopes,
127
+ package: (name) => packageScopes.find(p => p.name === name),
128
+ // A getter, so a repository whose config never mentions git spawns no git at all - and every
129
+ // command resolves config, not just the ones that care.
130
+ get git() {
131
+ return (gitScope ??= readGitScope(repoDir));
132
+ },
133
+ };
134
+ const withVars = (pkg, config) => interpolateConfig(config, { pkg: scopeOf(pkg), repository, env: { ...process.env }, semver });
135
+ this.config = withVars(this.rootPackage, await resolveConfig(this.dirname, this.dirname, cache));
99
136
  for (const pkg of this.packages) {
100
- if (pkg === this.rootPackage)
101
- continue;
102
- pkg.config = await resolveConfig(this.dirname, pkg.dirname, cache);
137
+ pkg.config = withVars(pkg, await resolveConfig(this.dirname, pkg.dirname, cache, pkg.name));
103
138
  }
139
+ if (this.monorepo)
140
+ this.rootPackage.config = this.config;
104
141
  }
105
142
  _updateDependencies() {
106
143
  const deps = {};
@@ -111,7 +148,7 @@ export class Repository extends Package {
111
148
  ...pkg.json.peerDependencies,
112
149
  ...pkg.json.optionalDependencies,
113
150
  };
114
- const configDeps = pkg.config.packages?.[pkg.name]?.dependencies;
151
+ const configDeps = pkg.config.dependencies;
115
152
  if (configDeps) {
116
153
  if (Array.isArray(configDeps))
117
154
  configDeps.forEach(x => (o[x] = o[x] || '*'));
@@ -205,3 +242,30 @@ function topoSortPackages(packages) {
205
242
  return 0;
206
243
  });
207
244
  }
245
+ /** `git` facts for a `${{ repository.git.* }}` expression. Synchronous on purpose: it backs a lazy
246
+ * getter, and a getter cannot await. Everything is `undefined` outside a git checkout - not an
247
+ * error, just a repository without one. */
248
+ function readGitScope(dirname) {
249
+ const run = (args) => {
250
+ try {
251
+ return execFileSync('git', args, { cwd: dirname, stdio: ['ignore', 'pipe', 'ignore'] })
252
+ .toString()
253
+ .trim();
254
+ }
255
+ catch {
256
+ return undefined;
257
+ }
258
+ };
259
+ const sha = run(['rev-parse', 'HEAD']);
260
+ if (sha === undefined)
261
+ return { branch: undefined, sha: undefined, shortSha: undefined, dirty: undefined };
262
+ const status = run(['status', '--porcelain']);
263
+ return {
264
+ // Empty on a detached HEAD, which is what a CI checkout often is - reported as undefined
265
+ // rather than an empty string, so `?? 'detached'` in an expression works.
266
+ branch: run(['branch', '--show-current']) || undefined,
267
+ sha,
268
+ shortSha: sha.slice(0, 7),
269
+ dirty: status === undefined ? undefined : status.length > 0,
270
+ };
271
+ }
@@ -16,10 +16,45 @@ export interface RmanConfig {
16
16
  changelog?: RmanConfig.ChangelogOptions;
17
17
  clean?: RmanConfig.CleanOptions;
18
18
  publish?: RmanConfig.PublishOptions;
19
- /** Keyed by npm script name (e.g. `"build"`, `"lint"`, `"test"`). */
20
- run?: Record<string, RmanConfig.RunScriptOptions>;
21
- /** Keyed by the in-repo package's own name. */
22
- packages?: Record<string, RmanConfig.PackageOptions>;
19
+ githubRelease?: RmanConfig.GithubReleaseOptions;
20
+ /** Keyed by npm script name (e.g. `"build"`, `"lint"`, `"test"`). A bare string (or array of
21
+ * them) is shorthand for `{ exec: ... }` - `test: "mocha"` and `test: { exec: "mocha" }` mean
22
+ * exactly the same thing. */
23
+ run?: Record<string, string | string[] | RmanConfig.RunScriptOptions>;
24
+ /** In-repo packages this one depends on beyond what its real `package.json` declares - purely
25
+ * for rman's own dependency graph (topo-sort, `--deps`/`--dependents`, `run`'s scheduling). An
26
+ * array defaults each entry's range to `"*"`; an object gives an explicit name -> range map.
27
+ * Declared from the root via a selector (`"[pkg-a]": { dependencies: [...] }`) or in the
28
+ * package's own `.rmanrc`. */
29
+ dependencies?: string[] | Record<string, string>;
30
+ /**
31
+ * Config for **other** packages, keyed by a `"[selector]"` naming them - `"[*]"` for every
32
+ * package in the repository, `"[*-dialect]"` for a glob over package names, `"[pkg-a]"` for one.
33
+ * Everything else in this object configures the package of the directory declaring it, so this
34
+ * is the only way a `.rmanrc` speaks about anything but its own package - most usefully the
35
+ * repository root's, which otherwise configures the root package alone.
36
+ *
37
+ * ```yaml
38
+ * # the repository root's own .rmanrc.yml
39
+ * run:
40
+ * build:
41
+ * before: node support/generate.cjs # a repo-wide bookend, run once at the root
42
+ * "[*]":
43
+ * run:
44
+ * build:
45
+ * after: node ../../support/postbuild.cjs # run in each package's own directory
46
+ * ```
47
+ *
48
+ * In YAML the quotes are **required**: a bare `[*]` parses as a flow sequence, and `*` as an
49
+ * alias indicator. Precedence, lowest first: `"[*]"`, then other selectors in declaration order,
50
+ * then the package's own unmarked config.
51
+ *
52
+ * Recursive, mirroring the schema's own `"$ref": "#"`: whatever a `.rmanrc` may say about its own
53
+ * package it may say here about the ones it names - nested selectors included. Typed as
54
+ * `RmanConfig` rather than `unknown` so the contents are actually checked; `unknown` let any
55
+ * shape through, which is the opposite of the point.
56
+ */
57
+ [selector: `[${string}]`]: RmanConfig;
23
58
  }
24
59
  export declare namespace RmanConfig {
25
60
  interface VersionOptions {
@@ -35,9 +70,29 @@ export declare namespace RmanConfig {
35
70
  * any package's own `changelog.tagPattern`, or that package's changelog boundary will resolve
36
71
  * to the repository release instead of its own last release. */
37
72
  releaseTagPattern?: string;
38
- script?: string | string[];
39
- preScript?: string | string[];
40
- postScript?: string | string[];
73
+ /** Keep this package's Dockerfile `org.opencontainers.image.version` label in step with the
74
+ * version being written. Per-package cascaded. Default `true` - the label's value is, by
75
+ * specification, the version of the packaged software, so there is only ever one correct
76
+ * value for it, and `version` is what knows it. Only ever *rewrites* a label the Dockerfile
77
+ * already declares (never inserts one), and reads the same path `publish --target docker`
78
+ * builds from (`publish.docker.dockerfile`), so a package without one is a no-op. */
79
+ stampDockerfile?: boolean;
80
+ /** Source files whose `version` constant is rewritten to the version being written, in the same
81
+ * commit as the bump - paths relative to the package's own directory (e.g.
82
+ * `["src/constants.ts"]`). Per-package cascaded; a listed file a package doesn't have is a
83
+ * silent no-op, so one `"[*]"` declaration covers a repo where only some packages carry one.
84
+ *
85
+ * Stamping the source, not the build output: a build-time rewrite leaves the checked-in file
86
+ * claiming a placeholder, so anything running from source reports that placeholder, git never
87
+ * records the released version, and the rewrite has to be redone on every build. */
88
+ stamp?: string | string[];
89
+ /** Command(s) run as this package's own `version` npm-lifecycle step, when its `package.json`
90
+ * doesn't define one itself. An array runs them in sequence. */
91
+ exec?: string | string[];
92
+ /** Same, for `preversion`. */
93
+ before?: string | string[];
94
+ /** Same, for `postversion`. */
95
+ after?: string | string[];
41
96
  }
42
97
  interface ChangelogOptions {
43
98
  ignoreTypes?: string[];
@@ -59,26 +114,36 @@ export declare namespace RmanConfig {
59
114
  changedSince?: string;
60
115
  skip?: boolean;
61
116
  if?: string;
62
- script?: string | string[];
63
- preScript?: string | string[];
64
- postScript?: string | string[];
117
+ /** Command(s) to run as this script itself, when the package's `package.json` doesn't define
118
+ * it. An array runs them in sequence. */
119
+ exec?: string | string[];
120
+ /** Same, for this script's `pre<script>` hook. */
121
+ before?: string | string[];
122
+ /** Same, for its `post<script>` hook. */
123
+ after?: string | string[];
65
124
  override?: boolean;
66
125
  }
67
- interface PackageOptions {
68
- dependencies?: string[] | Record<string, string>;
69
- }
70
126
  interface PublishOptions {
71
- /** Where `publish` should release this package to - default `['npm']` (every existing repo
127
+ /** Which **registry** `publish` ships this package to - default `['npm']` (every existing repo
72
128
  * keeps working unchanged). A package that only ever wants Docker images (typically also
73
- * `"private": true`, since it's not meant for npm at all) sets `['docker']`; a standalone app
74
- * shipped as GitHub Release assets - or deployed elsewhere entirely, with the release only
75
- * recording that it happened - sets `['github']`; any combination works (`['npm', 'github']`).
129
+ * `"private": true`, since it's not meant for npm at all) sets `['docker']`; both works too.
76
130
  * Each target answers "is this version already out there?" against its own registry, so a
77
- * package is never left without one: npm via `npm view`, docker via `docker manifest inspect`,
78
- * github via the release for that version's tag. */
131
+ * package is never left without one: npm via `npm view`, docker via `docker manifest inspect`.
132
+ *
133
+ * Note this is strictly about *package distribution*. The repository's GitHub Release is not
134
+ * a target here - it isn't a place a package ships to, it's the repository's own record that
135
+ * a release happened, and it is never opted into: see `githubRelease` and the
136
+ * `github-release` command. */
79
137
  target?: PublishTarget | PublishTarget[];
138
+ /** Where this package's publishable output lives, relative to its own directory (e.g.
139
+ * `"build"`). Per-package cascaded, so a root `"[*]"` block can say it once for the whole
140
+ * repository instead of repeating `publishConfig.directory` in every `package.json` - which
141
+ * still wins when a package declares it, being the more specific statement.
142
+ *
143
+ * Publishing from such a directory means the manifest there is **generated by `publish`**,
144
+ * from the package's own - see `PublishService`. There is nothing to configure about it. */
145
+ directory?: string;
80
146
  docker?: DockerPublishOptions;
81
- github?: GithubPublishOptions;
82
147
  /** Excludes this package from `publish` entirely (every target), regardless of
83
148
  * `target`/`"private"` - a single, explicit "never published" statement, e.g. for a package
84
149
  * released through some separate, unrelated process. `changelog` also skips it by default
@@ -87,7 +152,7 @@ export declare namespace RmanConfig {
87
152
  * package can still be meaningfully versioned without ever being published. */
88
153
  skip?: boolean;
89
154
  }
90
- type PublishTarget = 'npm' | 'docker' | 'github';
155
+ type PublishTarget = 'npm' | 'docker';
91
156
  /** Required once `"docker"` is one of this package's `publish.target`s - `publish --target
92
157
  * docker` errors clearly on a package that opts in here but leaves this out. */
93
158
  interface DockerPublishOptions {
@@ -112,19 +177,22 @@ export declare namespace RmanConfig {
112
177
  * full description, if present. Default `"DOCKER_README.md"`. */
113
178
  readme?: string;
114
179
  }
115
- /** Optional even when `"github"` is one of this package's `publish.target`s - unlike docker,
116
- * every required fact (which tag, which repository, what release notes) already has a sensible
117
- * source, so a bare `"target": ["github"]` is a complete configuration on its own. */
118
- interface GithubPublishOptions {
180
+ /** Entirely optional - `github-release` needs no configuration at all, since every required fact
181
+ * (which tag, which repository, what the notes say) already has a sensible source. Nothing here
182
+ * decides *whether* a release is cut: a release records that the repository shipped, so it is
183
+ * always cut, and these are only details about how. */
184
+ interface GithubReleaseOptions {
119
185
  /** Files to attach to the release, as glob patterns relative to the package's own directory
120
- * (e.g. `["dist/*.tar.gz"]`). A release with no assets is still perfectly valid - it records
121
- * that the version shipped, which is all a deploy-elsewhere package needs. */
186
+ * (e.g. `["dist/*.tar.gz"]`). Read from **every** package, since one release covers the whole
187
+ * source tree. A release with no assets at all is still perfectly valid - it records that the
188
+ * version shipped, which is all a deploy-elsewhere package needs. */
122
189
  assets?: string[];
123
- /** `owner/repo`. Default: parsed from the `origin` remote's URL. */
190
+ /** `owner/repo`. Default: parsed from the `origin` remote's URL. Root-level only. */
124
191
  repository?: string;
125
- /** Create the release as an unpublished draft. Default `false`. */
192
+ /** Create the release as an unpublished draft. Default `false`. Root-level only. */
126
193
  draft?: boolean;
127
- /** Default: whether the version being released is itself a semver prerelease (`1.3.0-beta.0`). */
194
+ /** Default: whether the version being released is itself a semver prerelease (`1.3.0-beta.0`).
195
+ * Root-level only. */
128
196
  prerelease?: boolean;
129
197
  }
130
198
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "1.0.9",
4
+ "version": "1.0.12",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {