rman 2.14.0 → 2.15.1

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
@@ -88,6 +88,7 @@ worked examples of every single command, see **[docs/cli-rman.md](https://github
88
88
  | [`config`](#rman-config) | Prints the effective `.rmanrc` config for the current directory's package. |
89
89
  | [`diff [package]`](#rman-diff-package) | Shows the git diff since a package's (or the repo's) last release tag. |
90
90
  | [`changelog`](#rman-changelog) | Generates a changelog per package from unreleased commits. |
91
+ | [`deps [names..]`](#rman-deps-names) | Lists dependencies with a newer version; `-u` upgrades them. |
91
92
  | [`version [bump]`](#rman-version-bump) | Bumps versions of changed packages (and their dependents). |
92
93
  | [`publish`](#rman-publish) | Publishes every package whose version isn't on its registry yet. |
93
94
  | [`github-release`](#rman-github-release) | Creates the repository's GitHub Release for its release tag. |
@@ -129,16 +130,17 @@ Options shared across several commands:
129
130
 
130
131
  - **`skip`** (`.rmanrc`, per package): leave this package alone - honoured by every command that
131
132
  *acts* on packages, ignored by `list`, which reports on them.
132
- - **Package filtering** (`list`, `run`/`build`/`test`, `exec`, `version`, `changelog`, and a
133
- plugin's own commands): `--scope <glob>`, `--ignore <glob>`, `--deps`, `--dependents` - see
134
- [Package filtering](https://github.com/panates/rman/blob/main/docs/rman.md#package-filtering-scopeignoredepsdependents)
133
+ - **Package filtering** (`list`, `run`/`build`/`test`, `exec`, `version`, `changelog`, `publish`,
134
+ `deps`, `clean`, `ci`, and a plugin's own commands): `--scope <glob>`, `--ignore <glob>`, `--deps`, `--dependents` - see
135
+ [Package filtering](https://github.com/panates/rman/blob/main/docs/rman.md#package-filtering-scopeignoreplatformdepsdependents)
135
136
  for the full semantics. **`--scope /` is the repository's own root package** - the same `/`
136
137
  `.rmanrc`'s `"[/]"` block uses, and not a glob, so `--scope '*'` means the members and a glob
137
138
  never picks up the root by name.
138
139
  - **`--from-root`/`-r`** (every command that narrows to the package you are standing in -
139
- `run`/`build`/`test`, `exec`, `changelog`, `diff`): run against the whole repository instead.
140
+ `run`/`build`/`test`, `exec`, `clean`, `changelog`, `diff`, `config`): run against the whole
141
+ repository instead.
140
142
  - **Branch guard** (every command that mutates state or runs scripts - `run`/`build`/`test`,
141
- `exec`, `version`): `--allow-branch <glob>`, `--ignore-branch <glob>` -
143
+ `exec`, `version`, `publish`, `github-release`, `clean`, `ci`): `--allow-branch <glob>`, `--ignore-branch <glob>` -
142
144
  refuses to run unless (or if) the current git branch matches, the same idea as GitHub Actions'
143
145
  own `branches`/`branches-ignore` workflow filters.
144
146
 
@@ -147,7 +149,7 @@ Options shared across several commands:
147
149
  Lists packages in the repository (alias: `ls`).
148
150
 
149
151
  ```bash
150
- rman list # table: Package / Version / Private / Changed / Path
152
+ rman list # table: Package / Version / Group / Platform / Private / Publish / Changed / Path
151
153
  rman ls --short # just the bare package names
152
154
  rman list --json # full detail as JSON
153
155
  rman list --parseable # location::name::version::PRIVATE::STATUS lines, for scripting
@@ -158,12 +160,16 @@ rman list --changed-since HEAD~5
158
160
  rman list --scope '@myorg/*' --ignore '*-internal'
159
161
  ```
160
162
 
163
+ The table keeps each release group together, in `version`'s plan order (root, then groups, then
164
+ packages in no group), and indents a package by how deep it sits. `Group` appears only when it says
165
+ something; `Publish` names the targets a package ships to, or `-` when none is left. `--toposort`
166
+ orders by dependencies instead, and `--json` keeps the inventory's own order.
167
+
161
168
  ### `rman info`
162
169
 
163
170
  Prints local environment (OS/CPU/memory, Node, git) and repository information. A platform adds its
164
171
  own ecosystem's part - the `node` built-in reports whichever package manager
165
- `.rmanrc "packageManager"` names, plus the installed `rman` packages, and only in a repository that
166
- asked for it.
172
+ `.rmanrc "packageManager"` names (`npm` by default), plus the installed `rman` and `typescript`.
167
173
 
168
174
  ```bash
169
175
  rman info
@@ -239,16 +245,38 @@ rman diff pkg-a # since pkg-a's own last tag, scoped to its directory
239
245
 
240
246
  ### `rman changelog`
241
247
 
242
- Generates a changelog per package from unreleased commits, grouped into ✨ Features / 🐛 Bug Fixes
243
- / 🔧 Other Changes.
248
+ Generates a changelog per package from unreleased commits, with a section per Conventional Commits
249
+ type (✨ Features, 🐛 Bug Fixes, ⚡ Performance, ...; anything else under 💬 General Changes) -
250
+ renamed or extended with `.rmanrc "changelog.titles"`. With `changelog.groupBy: group`, one file per
251
+ release group instead of per package.
244
252
 
245
253
  ```bash
246
254
  rman changelog # auto-detects each package's own last release
247
255
  rman changelog --from a1b2c3d # since a specific commit, for every package
248
256
  rman changelog --write # prepend into each package's own CHANGELOG.md
249
257
  rman changelog --write --file-path docs/CHANGELOG.md
258
+ rman changelog --rebuild # regenerate the files from scratch instead of appending
259
+ ```
260
+
261
+ ### `rman deps [names..]`
262
+
263
+ Lists the dependencies that have a newer version on their registry - every package, the monorepo
264
+ root's own tooling included - and with `-u` writes the new ranges to the manifests, then checks that
265
+ they still install.
266
+
267
+ ```bash
268
+ rman deps # what could move, and what holds it back
269
+ rman deps -u # write the new ranges, then check that they install
270
+ rman deps --target major # across a major too
271
+ rman deps '@types/*' # only the dependencies matching this glob
250
272
  ```
251
273
 
274
+ **A dependency stays inside its major by default** (`deps.target: minor`). Taking a major is a
275
+ decision a person makes: `--target major` for one run, `.rmanrc "deps.target"` for a standing
276
+ answer, or `deps.targets` for one dependency. Only a caret or tilde range is rewritten; a version is
277
+ offered only when every rule rman can see allows it. See
278
+ [docs/cli/deps.md](https://github.com/panates/rman/blob/main/docs/cli/deps.md).
279
+
252
280
  ### `rman version [bump]`
253
281
 
254
282
  Bumps versions of changed packages (and their in-group dependents), grouped via `.rmanrc "group"`.
@@ -394,10 +422,11 @@ precedence: `package.json`'s own `"rman"` key, `.rmanrc.yml` (YAML), `.rmanrc` (
394
422
  the dotfile-style name), and `.rmanrc.cjs`/`.rmanrc.mjs`/`.rmanrc.js` for config that needs real
395
423
  logic (a JS module's default export).
396
424
 
397
- **Who a declaration is about** follows one rule: unmarked keys configure the package of the
398
- directory declaring them, and a `"[selector]"` block configures the packages it names. So the
399
- repository root's own keys are the *root package's* - which is where repo-wide settings are read
400
- from anyway - and they reach the other packages only through a selector.
425
+ **Who a declaration is about** follows one rule: what is written above reaches below, and a
426
+ `"[selector]"` block narrows the audience. An unmarked key configures its directory's package and
427
+ every package under it, so the repository root's own keys are the baseline for the whole repository.
428
+ `"[/]"` is the root package alone, `"[platform:node]"` every package of that technology, and a glob
429
+ such as `"[*]"` the packages below.
401
430
 
402
431
  ```yaml
403
432
  # .rmanrc.yml, at the repository root
@@ -405,6 +434,11 @@ packageManager: pnpm
405
434
  logLevel: info
406
435
  allowBranch: [main, release/*]
407
436
 
437
+ '[/]': # the root package alone - run's scheduling keys are read from here
438
+ run:
439
+ build:
440
+ concurrency: 4
441
+
408
442
  version:
409
443
  commitMessage: 'chore(release): v{version}'
410
444
 
@@ -418,7 +452,6 @@ version:
418
452
  run:
419
453
  test: mocha # a bare string is shorthand for { exec: mocha }
420
454
  build:
421
- concurrency: 4
422
455
  before: [rman run lint]
423
456
  exec: tsc -b tsconfig-build.json
424
457
  after: node ../../support/postbuild.cjs
@@ -462,15 +495,15 @@ used to cover them was removed.
462
495
 
463
496
  ## Programmatic API
464
497
 
465
- Every command above is a thin wrapper around an exported service function - call them directly
498
+ Every command above is a thin wrapper around an exported service - call them directly
466
499
  from your own Node.js scripts without shelling out to the `rman` binary:
467
500
 
468
501
  ```ts
469
- import { Repository, VersionService } from 'rman';
502
+ import { Repository, VersionPlanService } from 'rman';
470
503
 
471
504
  const repository = await Repository.create();
472
- const plan = await VersionService.getPlan(repository);
473
- await VersionService.applyPlan(repository, plan, { changelog: true, push: true });
505
+ const plan = await VersionPlanService.getPlanner(repository.app).getPlan(repository);
506
+ await repository.app.getService('version').applyPlan(plan, { changelog: true, push: true });
474
507
  ```
475
508
 
476
509
  Full reference, with detailed examples for every service (`VersionService`, `PublishService`,
@@ -9,7 +9,7 @@ import type { GitHelper } from '../../../../utils/git.js';
9
9
  * abstract because they are statements about an *ecosystem* rather than about releases. This is
10
10
  * npm's pair of answers.
11
11
  *
12
- * Registered through the plugin's `versionPlanner`, so `rman version`/`rman changed` work in a
12
+ * Registered through the plugin's `versionPlanner`, so `rman version` works in a
13
13
  * repository that registered the `node` built-in and say what is missing in one that did not.
14
14
  */
15
15
  export declare class NodeVersionPlanService extends VersionPlanService {
@@ -8,7 +8,7 @@ import { VersionPlanService } from '../../../../services/version-plan.service.js
8
8
  * abstract because they are statements about an *ecosystem* rather than about releases. This is
9
9
  * npm's pair of answers.
10
10
  *
11
- * Registered through the plugin's `versionPlanner`, so `rman version`/`rman changed` work in a
11
+ * Registered through the plugin's `versionPlanner`, so `rman version` works in a
12
12
  * repository that registered the `node` built-in and say what is missing in one that did not.
13
13
  */
14
14
  export class NodeVersionPlanService extends VersionPlanService {
@@ -13,7 +13,7 @@ const config = {
13
13
  target: 'cli',
14
14
  describe: 'Generate the changelog since this commit/hash, applied the same way to every package. ' +
15
15
  'Default (also "auto" explicitly): auto-detect per package from its own most recent release ' +
16
- 'tag - same as "version"/"changed" - falling back to the version its own ecosystem\'s ' +
16
+ 'tag - same as "version" - falling back to the version its own ecosystem\'s ' +
17
17
  "registry reports (no tag yet), then to its whole history for a package that's never been " +
18
18
  'released at all',
19
19
  type: 'string',
@@ -43,7 +43,7 @@ const config = {
43
43
  cliName: 'release-version',
44
44
  describe: 'The version these notes are for - what the entry heading shows. Default: read back from ' +
45
45
  "each package's own latest release tag, which is only right once that release is tagged. " +
46
- 'Pass it when generating notes ahead of the bump (e.g. from "changed --json" in CI), ' +
46
+ 'Pass it when generating notes ahead of the bump (e.g. from "version --json" in CI), ' +
47
47
  'otherwise the heading shows the previous release.',
48
48
  type: 'string',
49
49
  },
@@ -91,7 +91,8 @@ const config = {
91
91
  target: 'config',
92
92
  describe: "The release tag naming scheme each package's changelog boundary is detected from - " +
93
93
  '"{name}" is replaced with the package name (e.g. "{name}@*" for independent versioning). ' +
94
- 'Default "v*", one repo-wide tag resolved through git describe.',
94
+ 'Default: "v*" (one repo-wide tag, resolved through git describe) while the repository has one ' +
95
+ 'version line, "{name}@*" once it has several.',
95
96
  type: 'string',
96
97
  },
97
98
  /**
@@ -60,13 +60,19 @@ function printSystemInfo(systemInfo) {
60
60
  }
61
61
  function printRepositoryInfo(info) {
62
62
  console.log('', colors.whiteBright('Repository') + ':');
63
- console.log(' ' + colors.reset('Type') + ' :', colors.yellowBright(info.type === 'monorepo' ? 'Monorepo' : 'Single package'));
64
- console.log(' ' + colors.reset('Name') + ' :', colors.yellowBright(info.name || '(none)'));
65
- console.log(' ' + colors.reset('Version') + ' :', colors.yellowBright(info.version || '(none)'));
66
- console.log(' ' + colors.reset('Root') + ' :', colors.yellowBright(info.root));
63
+ const rows = [
64
+ ['Type', colors.yellowBright(info.type === 'monorepo' ? 'Monorepo' : 'Single package')],
65
+ ['Name', colors.yellowBright(info.name || '(none)')],
66
+ ['Version', colors.yellowBright(info.version || '(none)')],
67
+ ['Root', colors.yellowBright(info.root)],
68
+ ['Platforms', colors.yellowBright(info.platforms.join(', ') || '(none)')],
69
+ ];
67
70
  if (info.type === 'monorepo') {
68
- console.log(' ' + colors.reset('Packages') + ' :', colors.yellowBright(String(info.packageCount)), colors.gray('(run "list" to see them)'));
71
+ rows.push(['Packages', colors.yellowBright(String(info.packageCount)), colors.gray('(run "list" to see them)')]);
69
72
  }
73
+ const width = Math.max(...rows.map(([label]) => label.length));
74
+ for (const [label, ...values] of rows)
75
+ console.log(' ' + colors.reset(label.padEnd(width)) + ' :', ...values);
70
76
  }
71
77
  /**
72
78
  * `rman info` - environment and repository, and back in the core because most of what it reports
@@ -210,6 +210,8 @@ const versionCommand = registerCommand(app => {
210
210
  name: e.package.name,
211
211
  selector: e.package.selector,
212
212
  isRoot: e.package.isRoot,
213
+ /** `rman list --json`'s spelling, so a consumer joins the two on it. */
214
+ groupKey: e.groupKey,
213
215
  group: e.group,
214
216
  status: e.status,
215
217
  from: e.from,
@@ -284,7 +286,8 @@ function printApplied(result) {
284
286
  console.log(`${colors.green('tags')} ${release.map(label).join(', ')} ${colors.gray('(repository release)')}`);
285
287
  }
286
288
  console.log(result.pushed
287
- ? `${colors.green('push')} pushed, with tags`
289
+ ? `${colors.green('push')} pushed the branch` +
290
+ (result.tags.length ? ` and ${result.tags.map(t => t.name).join(', ')}, together` : '')
288
291
  : `${colors.gray('push')} ${colors.gray('not pushed - run with --push, or push it yourself')}`);
289
292
  }
290
293
  function printPlan(entries) {
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.14.0';
1
+ export const version = '2.15.1';
@@ -53,7 +53,7 @@ export var Manifest;
53
53
  }
54
54
  }
55
55
  throw new Error(`No manifest provider recognizes "${dir}", so there is nowhere to write its version.\n` +
56
- ` A repository's ".rmanrc" names its technologies - see "plugins" (e.g. ['node']).`);
56
+ ` A repository's ".rmanrc" names its technologies - see "platforms", or "extends" (e.g. "rman:node").`);
57
57
  }
58
58
  Manifest.write = write;
59
59
  /**
@@ -172,7 +172,7 @@ export declare function isPlugin(value: unknown): value is Plugin;
172
172
  * - no `getBinPaths` -> nothing is prepended to a child process's PATH, so the inherited one
173
173
  * stands on its own rather than being guessed at;
174
174
  * - no `getRunSteps` -> a package's steps come from its `.rmanrc` alone, the core's only source;
175
- * - no `versionPlanner` -> `version`/`changed` fail naming the key, rather than releasing a
175
+ * - no `versionPlanner` -> `version` fails naming the key, rather than releasing a
176
176
  * plausible but untrue set of packages from a default nobody chose.
177
177
  */
178
178
  export declare const basePlatform: Platform;
@@ -73,7 +73,7 @@ export function isPlugin(value) {
73
73
  * - no `getBinPaths` -> nothing is prepended to a child process's PATH, so the inherited one
74
74
  * stands on its own rather than being guessed at;
75
75
  * - no `getRunSteps` -> a package's steps come from its `.rmanrc` alone, the core's only source;
76
- * - no `versionPlanner` -> `version`/`changed` fail naming the key, rather than releasing a
76
+ * - no `versionPlanner` -> `version` fails naming the key, rather than releasing a
77
77
  * plausible but untrue set of packages from a default nobody chose.
78
78
  */
79
79
  export const basePlatform = definePlatform({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.14.0",
4
+ "version": "2.15.1",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
@@ -106,8 +106,8 @@ export declare namespace ChangeHashService {
106
106
  * `changelog --from` uses, but reusable anywhere a command wants to answer "what changed for this
107
107
  * package". An explicit `options.from` (anything but `AUTO`) is returned as-is, applying the same
108
108
  * way to every package. Otherwise, it's auto-detected in order: (1) this package's own most recent
109
- * release tag - the same network-free `findLatestTag` lookup `version`/`changed` themselves use,
110
- * so all three commands agree on "since when" for any repo whose tags are the ones `rman version`
109
+ * release tag - the same network-free `findLatestTag` lookup `version` itself uses,
110
+ * so both commands agree on "since when" for any repo whose tags are the ones `rman version`
111
111
  * actually created; (2) failing that (no tag at all yet - e.g. onboarding `rman` onto a repo with
112
112
  * real release history but no `rman`-created tags), whatever this package's **own ecosystem**
113
113
  * reports as its published version (`Plugin.publishedVersion`), mapped to a git tag via
@@ -134,8 +134,8 @@ export var ChangeHashService;
134
134
  * `changelog --from` uses, but reusable anywhere a command wants to answer "what changed for this
135
135
  * package". An explicit `options.from` (anything but `AUTO`) is returned as-is, applying the same
136
136
  * way to every package. Otherwise, it's auto-detected in order: (1) this package's own most recent
137
- * release tag - the same network-free `findLatestTag` lookup `version`/`changed` themselves use,
138
- * so all three commands agree on "since when" for any repo whose tags are the ones `rman version`
137
+ * release tag - the same network-free `findLatestTag` lookup `version` itself uses,
138
+ * so both commands agree on "since when" for any repo whose tags are the ones `rman version`
139
139
  * actually created; (2) failing that (no tag at all yet - e.g. onboarding `rman` onto a repo with
140
140
  * real release history but no `rman`-created tags), whatever this package's **own ecosystem**
141
141
  * reports as its published version (`Plugin.publishedVersion`), mapped to a git tag via
@@ -7,6 +7,9 @@ export declare namespace SystemInfo {
7
7
  version: string;
8
8
  root: string;
9
9
  packageCount: number;
10
+ /** The technologies this run loaded, by `Platform.name`, in the order a directory is offered to
11
+ * them - so `["cargo", "node"]` means a directory both recognize is a Cargo package. */
12
+ platforms: string[];
10
13
  }
11
14
  /** Whatever `envinfo` reports, grouped by category - shape is `envinfo`'s own, not ours. */
12
15
  type SystemInfo = Record<string, Record<string, unknown>>;
@@ -23,6 +23,7 @@ export var SystemInfo;
23
23
  version: repository.rootPackage.version,
24
24
  root: repository.dirname,
25
25
  packageCount: repository.getPackages().length,
26
+ platforms: [...repository.app.platforms].map(p => p.name),
26
27
  };
27
28
  }
28
29
  SystemInfo.getRepositoryInfo = getRepositoryInfo;
@@ -56,7 +56,8 @@ export declare abstract class VersionPlanService {
56
56
  * Across groups: a package depending on another group's bumped package always gets its scheme's
57
57
  * **smallest** bump of its own (never inheriting the source's) - the dependency reference itself
58
58
  * is the only thing that changed for it. Whether that re-triggers its own group's cascade is
59
- * `cascade`'s answer for that smallest bump (`'changed'` under npm, so it does not), but it can
59
+ * `cascade`'s answer for that smallest bump (`'dependents'` under npm, so its own in-group
60
+ * dependents move with it), and it can
60
61
  * itself ripple into a third group, and so on, until nothing new is affected - see
61
62
  * `rippleCrossGroup`.
62
63
  *
@@ -58,7 +58,8 @@ export class VersionPlanService {
58
58
  * Across groups: a package depending on another group's bumped package always gets its scheme's
59
59
  * **smallest** bump of its own (never inheriting the source's) - the dependency reference itself
60
60
  * is the only thing that changed for it. Whether that re-triggers its own group's cascade is
61
- * `cascade`'s answer for that smallest bump (`'changed'` under npm, so it does not), but it can
61
+ * `cascade`'s answer for that smallest bump (`'dependents'` under npm, so its own in-group
62
+ * dependents move with it), and it can
62
63
  * itself ripple into a third group, and so on, until nothing new is affected - see
63
64
  * `rippleCrossGroup`.
64
65
  *
@@ -174,8 +175,8 @@ export class VersionPlanService {
174
175
  * The abort case, marked **after** the root entry so the whole table reads as one coherent
175
176
  * "if this were committed" preview rather than a mixture of two.
176
177
  *
177
- * `to` survives the overwrite; `status` is what every writer filters on (`VersionService`,
178
- * `changed`), so an `'error'` entry carrying a version cannot be applied by any of them - and
178
+ * `to` survives the overwrite; `status` is what every writer filters on (`VersionService`),
179
+ * so an `'error'` entry carrying a version cannot be applied by any of them - and
179
180
  * `version` throws on the first error before it would reach `applyPlan` anyway. The reason
180
181
  * keeps the boundary it was computed from, because the status column already says `error` and
181
182
  * a reader losing `changed since <tag>` loses the only thing that explains the number.
@@ -523,8 +524,9 @@ export class VersionPlanService {
523
524
  function getPlanner(app) {
524
525
  const planner = app.versionPlanner;
525
526
  if (!planner) {
526
- throw new Error('No version planner is registered, so no version plan can be computed. Name a plugin that ' +
527
- 'contributes one in .rmanrc "plugins" - "rman-node" for a Node repository.');
527
+ throw new Error('No version planner is registered, so no version plan can be computed. Name a technology ' +
528
+ 'that contributes one in .rmanrc "platforms", or reach it through "extends" - "rman:node" ' +
529
+ 'for a Node repository, which is laid down by default unless presets were turned off.');
528
530
  }
529
531
  return planner;
530
532
  }
@@ -254,8 +254,21 @@ export class VersionService extends Service {
254
254
  tagged.push({ name: releaseTag, created: true, release: true });
255
255
  }
256
256
  const pushed = !!options.push && bumped.length > 0;
257
- if (pushed)
258
- await git.push();
257
+ if (pushed) {
258
+ /** Every tag this release names, including one that was already there: it is this release's
259
+ * tag all the same, and a remote that lacks it is missing part of the release. */
260
+ const tags = tagged.map(t => t.name);
261
+ try {
262
+ await git.push({ tags });
263
+ }
264
+ catch (e) {
265
+ throw new Error(`${e.message}\n\n` +
266
+ ` Nothing was pushed - the branch and its tags go up together or not at all. Left behind ` +
267
+ `locally: ${commits.map(c => c.sha).join(', ') || 'no commit'}` +
268
+ (tags.length ? `, tagged ${tags.join(', ')}` : '') +
269
+ `.\n Once the cause is fixed, push them with: git push --atomic origin HEAD ${tags.join(' ')}`.trimEnd(), { cause: e });
270
+ }
271
+ }
259
272
  return { entries: plan, updated: bumped, commits, tags: tagged, pushed };
260
273
  }
261
274
  }
@@ -361,8 +374,8 @@ export class VersionService extends Service {
361
374
  (pkg.provider
362
375
  ? ` The "${pkg.provider}" provider looked and found no version to change. Name the ` +
363
376
  `identifier with { file, constant } if it is not "version", or drop the entry.`
364
- : ` This package belongs to no ecosystem (no plugin claimed it), so nothing knows how ` +
365
- `a version is declared in it. Name a plugin in .rmanrc "plugins".`));
377
+ : ` This package belongs to no ecosystem (no platform claimed it), so nothing knows how ` +
378
+ `a version is declared in it. Name the technology in .rmanrc "platforms", or reach it through "extends" (e.g. "rman:node").`));
366
379
  }
367
380
  /** Matched, but already reads the target - nothing to write and nothing to report. Distinct
368
381
  * from the `undefined` above, which is "nothing here to rewrite at all". */
@@ -447,8 +460,8 @@ function assertStampable(pkg, version) {
447
460
  (pkg.provider
448
461
  ? ` The "${pkg.provider}" provider looked and found no version to change. Name the ` +
449
462
  `identifier with { file, constant } if it is not "version", or drop the entry.`
450
- : ` This package belongs to no ecosystem (no plugin claimed it), so nothing knows how a ` +
451
- `version is declared in it. Name a plugin in .rmanrc "plugins".`));
463
+ : ` This package belongs to no ecosystem (no platform claimed it), so nothing knows how a ` +
464
+ `version is declared in it. Name the technology in .rmanrc "platforms", or reach it through "extends" (e.g. "rman:node").`));
452
465
  }
453
466
  }
454
467
  /**
package/utils/git.d.ts CHANGED
@@ -121,11 +121,13 @@ export declare class GitHelper {
121
121
  * Throws if a tag with that name already exists - callers wanting idempotent tagging should
122
122
  * check `tagExists` first. */
123
123
  createTag(name: string, message?: string): Promise<void>;
124
- /** Pushes the current branch to `remote` (default `"origin"`), and its tags too unless
125
- * `options.tags` is `false`. */
124
+ /**
125
+ * Pushes the current branch to `remote` (default `"origin"`) together with `options.tags`, by
126
+ * name, in **one atomic push**: the branch and every tag go up, or none of them does.
127
+ */
126
128
  push(options?: {
127
129
  remote?: string;
128
- tags?: boolean;
130
+ tags?: readonly string[];
129
131
  }): Promise<void>;
130
132
  /** `git remote get-url <remote>` (default `"origin"`) - `undefined` when that remote isn't
131
133
  * configured at all, which is a perfectly normal state for a local-only repository. */
package/utils/git.js CHANGED
@@ -394,14 +394,24 @@ export class GitHelper {
394
394
  throw new Error(`Unable to create tag "${name}": ${e.message}`, { cause: e });
395
395
  }
396
396
  }
397
- /** Pushes the current branch to `remote` (default `"origin"`), and its tags too unless
398
- * `options.tags` is `false`. */
397
+ /**
398
+ * Pushes the current branch to `remote` (default `"origin"`) together with `options.tags`, by
399
+ * name, in **one atomic push**: the branch and every tag go up, or none of them does.
400
+ */
401
+ /* **Named tags, not `--tags`.** `git push <remote> --tags` sends every tag in the clone, so one
402
+ * stale tag from months ago - re-created locally, diverged from the remote's - failed every
403
+ * release from then on, and failed it *after* the new tag had already gone up. Measured on
404
+ * `postgrejs` with 2.15.0: `v3.13.0` pushed, `v3.10.0` rejected, exit 1, and in CI the publish
405
+ * step after it never ran.
406
+ *
407
+ * **One command, `--atomic`.** The branch used to go first and the tags second, so a failure in
408
+ * the second - GitHub's own `fatal error in commit_refs` did it to the same repository an hour
409
+ * earlier - left a pushed version bump with no tag, which a re-run cannot repair because it has
410
+ * nothing left to bump. Atomic, a failure leaves the remote exactly as it was. */
399
411
  async push(options) {
400
412
  const remote = options?.remote ?? 'origin';
401
413
  try {
402
- await execFileAsync('git', ['push', remote], { cwd: this.cwd });
403
- if (options?.tags !== false)
404
- await execFileAsync('git', ['push', remote, '--tags'], { cwd: this.cwd });
414
+ await execFileAsync('git', ['push', '--atomic', remote, 'HEAD', ...(options?.tags ?? [])], { cwd: this.cwd });
405
415
  }
406
416
  catch (e) {
407
417
  throw new Error(`Unable to push to "${remote}": ${e.message}`, { cause: e });
@@ -176,7 +176,7 @@ applySkip?: boolean): Package[];
176
176
  * **It only means anything where a command scopes by the current directory** - `run`/`build`/`test`,
177
177
  * `exec`, `clean`, `changelog` and `diff` narrow to `Repository.currentPackage` when you stand
178
178
  * inside a package, and this is the escape hatch. On a command that already works across the whole
179
- * repository (`version`, `publish`, `list`, `changed`) it would be a flag that does nothing, which
179
+ * repository (`version`, `publish`, `list`) it would be a flag that does nothing, which
180
180
  * is worse than not offering it: a no-op flag reads as a promise.
181
181
  *
182
182
  * That same rule is why there is no `--root-only` beside it, however naturally the pair reads: it
@@ -210,7 +210,7 @@ applySkip = true) {
210
210
  * **It only means anything where a command scopes by the current directory** - `run`/`build`/`test`,
211
211
  * `exec`, `clean`, `changelog` and `diff` narrow to `Repository.currentPackage` when you stand
212
212
  * inside a package, and this is the escape hatch. On a command that already works across the whole
213
- * repository (`version`, `publish`, `list`, `changed`) it would be a flag that does nothing, which
213
+ * repository (`version`, `publish`, `list`) it would be a flag that does nothing, which
214
214
  * is worse than not offering it: a no-op flag reads as a promise.
215
215
  *
216
216
  * That same rule is why there is no `--root-only` beside it, however naturally the pair reads: it