rman 2.16.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/builtins/platforms/node/augmentation/rmanrc.augmentation.d.ts +5 -2
- package/builtins/platforms/node/augmentation/system-info.augmentation.js +7 -1
- package/builtins/platforms/node/node-config.interface.d.ts +10 -23
- package/builtins/platforms/node/services/ci.service.d.ts +6 -3
- package/builtins/platforms/node/services/ci.service.js +19 -8
- package/builtins/platforms/node/services/publish.service.js +1 -2
- package/builtins/platforms/node/utils/copy-assets.d.ts +30 -0
- package/builtins/platforms/node/utils/copy-assets.js +104 -0
- package/builtins/publish-targets/docker/docker-publish.service.js +36 -1
- package/builtins/publish-targets/docker/docker.target.js +2 -0
- package/commands/publish.command.js +83 -12
- package/constants.js +1 -1
- package/core/interfaces/publish-target.d.ts +5 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/interfaces/rman-config.interface.d.ts +12 -0
- package/package.json +4 -12
- package/utils/git.js +13 -7
package/README.md
CHANGED
|
@@ -169,7 +169,7 @@ orders by dependencies instead, and `--json` keeps the inventory's own order.
|
|
|
169
169
|
|
|
170
170
|
Prints local environment (OS/CPU/memory, Node, git) and repository information. A platform adds its
|
|
171
171
|
own ecosystem's part - the `node` built-in reports whichever package manager
|
|
172
|
-
`.rmanrc "packageManager"` names (`npm` by default), plus the installed `rman` and `typescript`.
|
|
172
|
+
`.rmanrc "packageManager.node"` names (`npm` by default), plus the installed `rman` and `typescript`.
|
|
173
173
|
|
|
174
174
|
```bash
|
|
175
175
|
rman info
|
|
@@ -430,7 +430,8 @@ such as `"[*]"` the packages below.
|
|
|
430
430
|
|
|
431
431
|
```yaml
|
|
432
432
|
# .rmanrc.yml, at the repository root
|
|
433
|
-
packageManager:
|
|
433
|
+
packageManager:
|
|
434
|
+
node: pnpm
|
|
434
435
|
logLevel: info
|
|
435
436
|
allowBranch: [main, release/*]
|
|
436
437
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { cleanCommand, CleanExtraKeys } from '../commands/clean.command.js';
|
|
2
|
-
import type { NodeConfigKeys, RmanNodeConfig } from '../node-config.interface.js';
|
|
2
|
+
import type { NodeConfigKeys, NodePackageManagers, RmanNodeConfig } from '../node-config.interface.js';
|
|
3
3
|
import type { CiService } from '../services/ci.service.js';
|
|
4
4
|
/**
|
|
5
5
|
* **Every type this plugin adds to rman, in one `declare module` block.**
|
|
@@ -25,6 +25,9 @@ declare module '../../../../interfaces/rman-config.interface.js' {
|
|
|
25
25
|
*/
|
|
26
26
|
interface RmanConfigKeys extends NodeConfigKeys {
|
|
27
27
|
}
|
|
28
|
+
/** Node's entry in `.rmanrc "packageManager"` - `{ node: 'pnpm' }`. */
|
|
29
|
+
interface PackageManagers extends NodePackageManagers {
|
|
30
|
+
}
|
|
28
31
|
/**
|
|
29
32
|
* **`clean.*`, contributed by the command that reads it**, the way every built-in contributes its
|
|
30
33
|
* own key - `skip` derived from the command's `config` block, `include`/`exclude` hand-written in
|
|
@@ -74,7 +77,7 @@ declare module '../../../../services/system-info.js' {
|
|
|
74
77
|
/**
|
|
75
78
|
* Report this package manager's version under `Binaries`, plus the `npmPackages` sections.
|
|
76
79
|
*
|
|
77
|
-
* Defaults to `.rmanrc "packageManager"` (read off `Options.repository`), then to `npm` -
|
|
80
|
+
* Defaults to `.rmanrc "packageManager.node"` (read off `Options.repository`), then to `npm` -
|
|
78
81
|
* this package being installed is itself the statement that the repository is a Node one.
|
|
79
82
|
*/
|
|
80
83
|
packageManager?: PackageManager;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SystemInfo as OrgSystemInfo } from '../../../../services/system-info.js';
|
|
2
|
+
import { CiService } from '../services/ci.service.js';
|
|
2
3
|
/**
|
|
3
4
|
* Adds the npm half of `SystemInfo` to rman's own types.
|
|
4
5
|
*
|
|
@@ -41,7 +42,12 @@ export function augmentSystemInfo() {
|
|
|
41
42
|
/** An explicit argument first, then the repository's own config, then npm. The config step is
|
|
42
43
|
* why the core declares `Options.repository`: `rman info` passes only that, so without it a
|
|
43
44
|
* pnpm repository would silently be reported as an npm one. */
|
|
44
|
-
|
|
45
|
+
/** The old bare `packageManager: pnpm` is refused here as `ci` and `publish` refuse it - read
|
|
46
|
+
* as a map it has no `node` entry, and `info` would report npm for a pnpm repository. */
|
|
47
|
+
const configured = options?.repository?.config?.packageManager;
|
|
48
|
+
if (typeof configured === 'string')
|
|
49
|
+
CiService.resolvePackageManager(options.repository);
|
|
50
|
+
const packageManager = options?.packageManager ?? asPackageManager(configured?.node) ?? 'npm';
|
|
45
51
|
return base({
|
|
46
52
|
...options,
|
|
47
53
|
envinfo: {
|
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
import type { ConfigValue, RmanConfig, ScopedVars } from '../../../interfaces/rman-config.interface.js';
|
|
2
2
|
import type { CiService } from './services/ci.service.js';
|
|
3
3
|
/**
|
|
4
|
-
* The `.rmanrc` keys that only mean something because the repository is a Node one
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* *and* by the `npm` publish target, so neither can contribute it. Everything else this package
|
|
8
|
-
* adds to `.rmanrc` is declared by whoever reads it - `clean.*` by `clean.command.ts` (through
|
|
9
|
-
* `CommandContribution`), `publish.npm.*` by the target, below.
|
|
10
|
-
*
|
|
11
|
-
* **Two surfaces, and they are not alternatives:**
|
|
12
|
-
*
|
|
13
|
-
* - the `declare module 'rman'` block in [`../augmentation/rman.augmentation.ts`] merges this into
|
|
14
|
-
* `RmanConfigKeys`, so `repository.config.packageManager` stays typed wherever it is read with no
|
|
15
|
-
* cast. It lives there because one such block per package is the limit;
|
|
16
|
-
* - `RmanNodeConfig` is the name a *config author* annotates with, which is what makes the import
|
|
17
|
-
* carrying that augmentation explicit instead of a side effect someone has to remember.
|
|
4
|
+
* The `.rmanrc` keys that only mean something because the repository is a Node one, merged into
|
|
5
|
+
* `RmanConfigKeys` by `rmanrc.augmentation.ts`. Everything else this built-in adds is declared by
|
|
6
|
+
* whoever reads it - `clean.*` by `clean.command.ts`, `publish.npm.*` by the target, below.
|
|
18
7
|
*/
|
|
19
8
|
export interface NodeConfigKeys {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*/
|
|
28
|
-
packageManager?: CiService.PackageManager;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Node's entry in `.rmanrc "packageManager"`: which package manager `ci` installs with, `publish`
|
|
12
|
+
* publishes with and `info` reports - `packageManager: { node: pnpm }`. Default `npm`.
|
|
13
|
+
*/
|
|
14
|
+
export interface NodePackageManagers {
|
|
15
|
+
node?: CiService.PackageManager;
|
|
29
16
|
}
|
|
30
17
|
export declare namespace RmanNodeConfig {
|
|
31
18
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LogLevel } from '../../../../core/classes/logger.js';
|
|
2
|
+
import type { Package } from '../../../../core/classes/package.js';
|
|
2
3
|
import type { Repository } from '../../../../core/classes/repository.js';
|
|
3
4
|
import type { PackageFilterOptions } from '../../../../utils/package-filter.js';
|
|
4
5
|
export declare namespace CiService {
|
|
@@ -21,9 +22,11 @@ export declare namespace CiService {
|
|
|
21
22
|
* the root's `.rmanrc logLevel`, then 'info' - see `resolveRootLogLevel`. */
|
|
22
23
|
logLevel?: LogLevel;
|
|
23
24
|
}
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The package manager `pkg` uses: `cliValue` when given, else its `.rmanrc "packageManager.node"`,
|
|
27
|
+
* else `npm`. Pass the repository for the one the repository installs with.
|
|
28
|
+
*/
|
|
29
|
+
function resolvePackageManager(pkg: Package, cliValue?: PackageManager): PackageManager;
|
|
27
30
|
/** Deletes `node_modules` and any known lockfile directly under `dirname`. Returns the names
|
|
28
31
|
* that actually existed (and were removed), so the caller can log only those. */
|
|
29
32
|
function wipe(dirname: string): Promise<string[]>;
|
|
@@ -16,17 +16,28 @@ export var CiService;
|
|
|
16
16
|
* act on. One list, one place it is written down.
|
|
17
17
|
*/
|
|
18
18
|
CiService.PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'bun'];
|
|
19
|
-
/**
|
|
20
|
-
*
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The package manager `pkg` uses: `cliValue` when given, else its `.rmanrc "packageManager.node"`,
|
|
21
|
+
* else `npm`. Pass the repository for the one the repository installs with.
|
|
22
|
+
*/
|
|
23
|
+
/* **The bare string is refused rather than read.** `packageManager: pnpm` was the old spelling,
|
|
24
|
+
* and rman validates no config keys, so read as an object it would simply have no `node` entry -
|
|
25
|
+
* a pnpm repository installing with npm, saying nothing. No repository of this organization
|
|
26
|
+
* wrote it, so a clear refusal costs nobody anything. */
|
|
27
|
+
function resolvePackageManager(pkg, cliValue) {
|
|
22
28
|
if (cliValue)
|
|
23
29
|
return cliValue;
|
|
24
|
-
const configured =
|
|
25
|
-
if (configured ===
|
|
30
|
+
const configured = pkg.config?.packageManager;
|
|
31
|
+
if (typeof configured === 'string') {
|
|
32
|
+
throw new Error(`.rmanrc "packageManager" takes one entry per technology - write packageManager: { node: ${configured} } ` +
|
|
33
|
+
`instead of packageManager: ${configured}.`);
|
|
34
|
+
}
|
|
35
|
+
const node = configured?.node;
|
|
36
|
+
if (node === undefined)
|
|
26
37
|
return 'npm';
|
|
27
|
-
if (CiService.PACKAGE_MANAGERS.includes(
|
|
28
|
-
return
|
|
29
|
-
throw new Error(`Invalid "packageManager" in .rmanrc: "${
|
|
38
|
+
if (typeof node === 'string' && CiService.PACKAGE_MANAGERS.includes(node))
|
|
39
|
+
return node;
|
|
40
|
+
throw new Error(`Invalid "packageManager.node" in .rmanrc: "${String(node)}" (expected one of: ${CiService.PACKAGE_MANAGERS.join(', ')})`);
|
|
30
41
|
}
|
|
31
42
|
CiService.resolvePackageManager = resolvePackageManager;
|
|
32
43
|
/** Deletes `node_modules` and any known lockfile directly under `dirname`. Returns the names
|
|
@@ -305,7 +305,6 @@ export var PublishService;
|
|
|
305
305
|
* `consumerNeeds`).
|
|
306
306
|
*/
|
|
307
307
|
async function applyPlan(repository, plan, options = {}) {
|
|
308
|
-
const packageManager = CiService.resolvePackageManager(repository, options.packageManager);
|
|
309
308
|
const failed = new Set();
|
|
310
309
|
const result = [];
|
|
311
310
|
/** The tuple is explicit: `[p.name, p]` widens to `(string | Package)[]`, and whether `new Map`
|
|
@@ -331,7 +330,7 @@ export var PublishService;
|
|
|
331
330
|
/** The plan's own tag and staging decision, not recomputed ones: `getPlan` decided where
|
|
332
331
|
* this package goes, the reader confirmed that, and `applyPlan` is here to carry it out.
|
|
333
332
|
* `options.*` is the fallback only for a plan built by something other than `getPlan`. */
|
|
334
|
-
await exec(buildPublishCommand(packageManager, {
|
|
333
|
+
await exec(buildPublishCommand(CiService.resolvePackageManager(pkg, options.packageManager), {
|
|
335
334
|
...options,
|
|
336
335
|
tag: entry.distTag ?? options.tag,
|
|
337
336
|
staged: entry.staged ?? options.staged,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RunBinOptions, RunBinResult } from '../../../../utils/run-bin.js';
|
|
2
|
+
/**
|
|
3
|
+
* The files `copyAssets` copies when it is given no patterns: the data formats a package keeps
|
|
4
|
+
* beside its sources - translations, fixtures, schemas - that `tsc` does not emit.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_ASSET_PATTERNS: readonly string[];
|
|
7
|
+
export interface CopyAssetsOptions {
|
|
8
|
+
/** The tsconfig the build compiles with - its `rootDir` and `outDir` say where the files are and
|
|
9
|
+
* where they go. */
|
|
10
|
+
tsconfig: string;
|
|
11
|
+
/** Globs over paths relative to `rootDir`. Default `DEFAULT_ASSET_PATTERNS`. */
|
|
12
|
+
patterns?: readonly string[];
|
|
13
|
+
/** Runs `tsc` - a function step's own `ctx.runBin`, which already resolves the repository's
|
|
14
|
+
* installed copy. */
|
|
15
|
+
runBin: (bin: string, argv: string[], options?: RunBinOptions) => Promise<RunBinResult>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Copies the files under a tsconfig's `rootDir` that match `patterns` into its `outDir`, keeping
|
|
19
|
+
* their place in the tree - `src/i18n/tr.json` lands at `build/i18n/tr.json`, where the compiled
|
|
20
|
+
* code that reads it expects to find it. Returns the files it wrote, absolute.
|
|
21
|
+
*
|
|
22
|
+
* `tsc` emits none of them: it compiles sources, and a `.json` reaches the output only when code
|
|
23
|
+
* imports it under `resolveJsonModule`. A file read at run time - a translation, an XML template, a
|
|
24
|
+
* fixture - is left behind, and the built package fails on a path that exists only in `src`.
|
|
25
|
+
*
|
|
26
|
+
* A tsconfig with no `outDir` compiles beside its sources, so there is nothing to copy and nothing
|
|
27
|
+
* is. `node_modules`, the output directory itself, `tsconfig*.json`, `package.json` and
|
|
28
|
+
* `package-lock.json` are never copied.
|
|
29
|
+
*/
|
|
30
|
+
export declare function copyAssets(options: CopyAssetsOptions): Promise<string[]>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import glob from 'fast-glob';
|
|
4
|
+
/**
|
|
5
|
+
* The files `copyAssets` copies when it is given no patterns: the data formats a package keeps
|
|
6
|
+
* beside its sources - translations, fixtures, schemas - that `tsc` does not emit.
|
|
7
|
+
*/
|
|
8
|
+
/* **A list, not "everything that is not TypeScript"** - the user's call, and the reason is what
|
|
9
|
+
* ships: a published package is what lands in a consumer's `node_modules`, and "every other file"
|
|
10
|
+
* puts a stray `.md`, a `.DS_Store` or a test's data file there with nothing saying so. A list
|
|
11
|
+
* leaves out what nobody named, and a repository that needs more names it. */
|
|
12
|
+
export const DEFAULT_ASSET_PATTERNS = ['**/*.json', '**/*.xml', '**/*.yaml', '**/*.yml'];
|
|
13
|
+
/**
|
|
14
|
+
* Copies the files under a tsconfig's `rootDir` that match `patterns` into its `outDir`, keeping
|
|
15
|
+
* their place in the tree - `src/i18n/tr.json` lands at `build/i18n/tr.json`, where the compiled
|
|
16
|
+
* code that reads it expects to find it. Returns the files it wrote, absolute.
|
|
17
|
+
*
|
|
18
|
+
* `tsc` emits none of them: it compiles sources, and a `.json` reaches the output only when code
|
|
19
|
+
* imports it under `resolveJsonModule`. A file read at run time - a translation, an XML template, a
|
|
20
|
+
* fixture - is left behind, and the built package fails on a path that exists only in `src`.
|
|
21
|
+
*
|
|
22
|
+
* A tsconfig with no `outDir` compiles beside its sources, so there is nothing to copy and nothing
|
|
23
|
+
* is. `node_modules`, the output directory itself, `tsconfig*.json`, `package.json` and
|
|
24
|
+
* `package-lock.json` are never copied.
|
|
25
|
+
*/
|
|
26
|
+
/* **`rootDir`/`outDir` are asked of `tsc --showConfig`, not read out of the file.** A package's
|
|
27
|
+
* tsconfig usually inherits both through `extends` - a relative path or a package such as
|
|
28
|
+
* `@panates/tsconfig` - and resolving that chain is `tsc`'s own logic: JSON with comments, a list
|
|
29
|
+
* of bases, package `exports`. `--showConfig` prints the resolved result with every path relative
|
|
30
|
+
* to the config it was given, in about 0.4s (measured on `panates/opra`'s `common`), which is small
|
|
31
|
+
* beside the compile it follows.
|
|
32
|
+
*
|
|
33
|
+
* **No `rootDir` means `tsc` computed one** - the common directory of its inputs - and `--showConfig`
|
|
34
|
+
* lists those inputs under `files`, so the same answer is taken from there. Declaration files are
|
|
35
|
+
* left out of it, as `tsc` leaves them out. */
|
|
36
|
+
export async function copyAssets(options) {
|
|
37
|
+
const configDir = path.dirname(path.resolve(options.tsconfig));
|
|
38
|
+
/** **`onLine` swallows the lines**: a step's `runBin` hands every line to the step's log, and the
|
|
39
|
+
* whole resolved configuration printed under the package is noise. `output` still collects them. */
|
|
40
|
+
let output;
|
|
41
|
+
try {
|
|
42
|
+
({ output } = await options.runBin('tsc', ['--showConfig', '-p', options.tsconfig], {
|
|
43
|
+
stdio: 'pipe',
|
|
44
|
+
onLine: () => undefined,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
throw new Error(`"tsc --showConfig -p ${options.tsconfig}" failed:\n${String(e?.output ?? e?.message).trim()}`, {
|
|
49
|
+
cause: e,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const config = parseShowConfig(output, options.tsconfig);
|
|
53
|
+
const outDir = config.compilerOptions?.outDir;
|
|
54
|
+
if (!outDir)
|
|
55
|
+
return [];
|
|
56
|
+
const out = path.resolve(configDir, outDir);
|
|
57
|
+
const root = config.compilerOptions?.rootDir
|
|
58
|
+
? path.resolve(configDir, config.compilerOptions.rootDir)
|
|
59
|
+
: (commonDir((config.files ?? []).filter(f => !/\.d\.[cm]?ts$/.test(f)).map(f => path.resolve(configDir, f))) ??
|
|
60
|
+
configDir);
|
|
61
|
+
/** `package.json` and its lockfile because a `rootDir` of the package itself would otherwise send
|
|
62
|
+
* them along - and the build directory's manifest is `publish`'s (or the preset's) to write. */
|
|
63
|
+
const ignore = ['**/node_modules/**', '**/tsconfig*.json', '**/package.json', '**/package-lock.json'];
|
|
64
|
+
const outInsideRoot = path.relative(root, out);
|
|
65
|
+
if (outInsideRoot && !outInsideRoot.startsWith('..') && !path.isAbsolute(outInsideRoot)) {
|
|
66
|
+
ignore.push(`${outInsideRoot.split(path.sep).join('/')}/**`);
|
|
67
|
+
}
|
|
68
|
+
const files = await glob([...(options.patterns ?? DEFAULT_ASSET_PATTERNS)], {
|
|
69
|
+
cwd: root,
|
|
70
|
+
ignore,
|
|
71
|
+
onlyFiles: true,
|
|
72
|
+
dot: false,
|
|
73
|
+
});
|
|
74
|
+
const written = [];
|
|
75
|
+
for (const rel of files.sort()) {
|
|
76
|
+
const target = path.join(out, rel);
|
|
77
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
78
|
+
fs.copyFileSync(path.join(root, rel), target);
|
|
79
|
+
written.push(target);
|
|
80
|
+
}
|
|
81
|
+
return written;
|
|
82
|
+
}
|
|
83
|
+
/** `tsc --showConfig`'s answer. It prints the JSON alone on success; anything else is a tsconfig
|
|
84
|
+
* `tsc` could not read, which is reported with what it said. */
|
|
85
|
+
function parseShowConfig(output, tsconfig) {
|
|
86
|
+
const start = output.indexOf('{');
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(output.slice(start));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error(`"tsc --showConfig -p ${tsconfig}" did not print a configuration:\n${output.trim()}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** The deepest directory holding every one of `files`, or `undefined` for none. */
|
|
95
|
+
function commonDir(files) {
|
|
96
|
+
if (!files.length)
|
|
97
|
+
return undefined;
|
|
98
|
+
let common = path.dirname(files[0]);
|
|
99
|
+
for (const file of files.slice(1)) {
|
|
100
|
+
while (path.relative(common, file).startsWith('..'))
|
|
101
|
+
common = path.dirname(common);
|
|
102
|
+
}
|
|
103
|
+
return common;
|
|
104
|
+
}
|
|
@@ -183,7 +183,42 @@ async function buildAndPush(repository, entry) {
|
|
|
183
183
|
args.push('--build-arg', `${name}="${expandEnvValue(value)}"`);
|
|
184
184
|
}
|
|
185
185
|
args.push('-f', `"${dockerfile}"`, '-t', `"${entry.image}:${entry.version}"`, '-t', `"${entry.image}:latest"`, '--push', '.');
|
|
186
|
-
|
|
186
|
+
/** **Shown as it runs and kept**, so a failure can say why. It ran with `stdio: 'inherit'`, and the
|
|
187
|
+
* one thing left to report was the exit code - measured on `panates/syncbridge`, the line that
|
|
188
|
+
* explained the failure sat forty lines above the plan's recap and the recap said
|
|
189
|
+
* `Command failed (1)`. On stderr, so a `--json` stdout stays one document. */
|
|
190
|
+
const lines = [];
|
|
191
|
+
const result = await exec(`docker ${args.join(' ')}`, {
|
|
192
|
+
cwd,
|
|
193
|
+
app: repository.app,
|
|
194
|
+
throwOnError: false,
|
|
195
|
+
onLine: line => {
|
|
196
|
+
lines.push(line);
|
|
197
|
+
process.stderr.write(line + '\n');
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
if (result.code)
|
|
201
|
+
throw new Error(`docker build exited with code ${result.code}${failureLines(lines)}`);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The lines of a failed build worth putting in a one-paragraph reason: those saying "error", with
|
|
205
|
+
* BuildKit's `#42 31.01 ` step prefix removed, each once - BuildKit prints a failing step's output
|
|
206
|
+
* twice, once as it runs and again in its summary - and without npm's pointer to its own log file.
|
|
207
|
+
* At most four, as an indented block under the first line.
|
|
208
|
+
*/
|
|
209
|
+
function failureLines(lines) {
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
for (const raw of lines) {
|
|
212
|
+
const line = raw
|
|
213
|
+
.replace(/^#\d+\s+[\d.]+\s+/, '')
|
|
214
|
+
.replace(/^[\d.]+\s+/, '')
|
|
215
|
+
.trim();
|
|
216
|
+
if (!/\berror\b/i.test(line) || /complete log of this run/i.test(line))
|
|
217
|
+
continue;
|
|
218
|
+
seen.add(line);
|
|
219
|
+
}
|
|
220
|
+
const picked = [...seen].slice(0, 4);
|
|
221
|
+
return picked.length ? ':\n' + picked.map(l => ` ${l}`).join('\n') : '';
|
|
187
222
|
}
|
|
188
223
|
async function updateDescription(entry) {
|
|
189
224
|
const pkg = entry.package;
|
|
@@ -14,6 +14,8 @@ import { DOCKER_TARGET } from './docker-publish.service.js';
|
|
|
14
14
|
export const dockerPublishTarget = {
|
|
15
15
|
name: DOCKER_TARGET,
|
|
16
16
|
describe: 'Build and push a container image (docker buildx build --push)',
|
|
17
|
+
/** An image installs its dependencies from the registries the other targets push to. */
|
|
18
|
+
publishesLast: true,
|
|
17
19
|
options: {
|
|
18
20
|
dockerNamespace: {
|
|
19
21
|
target: 'cli',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import readline from 'node:readline/promises';
|
|
2
3
|
import colors from 'ansi-colors';
|
|
3
4
|
import { unknownTargets } from '../core/interfaces/publish-target.js';
|
|
@@ -109,7 +110,9 @@ const publishCommand = registerCommand(app => {
|
|
|
109
110
|
handler: async (args) => {
|
|
110
111
|
await assertAllowedBranch(repository, readBranchGuardOptions(args));
|
|
111
112
|
const requested = args.target;
|
|
112
|
-
|
|
113
|
+
/** A target built from what the others publish goes after them - see `publishesLast`. A
|
|
114
|
+
* stable sort, so registration order still decides among the rest. */
|
|
115
|
+
const selected = selectTargets(targets, requested).sort((a, b) => Number(!!a.publishesLast) - Number(!!b.publishesLast));
|
|
113
116
|
const ctx = {
|
|
114
117
|
app,
|
|
115
118
|
repository,
|
|
@@ -161,14 +164,21 @@ const publishCommand = registerCommand(app => {
|
|
|
161
164
|
}
|
|
162
165
|
if (!proceed)
|
|
163
166
|
return;
|
|
164
|
-
|
|
167
|
+
const outcomes = [];
|
|
165
168
|
for (const [target, plan] of plans) {
|
|
166
169
|
const applied = await target.applyPlan(ctx, plan);
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
outcomes.push(...printApplied(applied, plan, target.name));
|
|
171
|
+
}
|
|
172
|
+
if (!args.json)
|
|
173
|
+
printRecap(outcomes);
|
|
174
|
+
writeStepSummary(outcomes);
|
|
175
|
+
const failed = outcomes.filter(o => o.entry.status === 'error');
|
|
176
|
+
/** **The names are in the line itself**, not only in the recap above it: in CI stdout and
|
|
177
|
+
* stderr are separate pipes, and measured on `panates/syncbridge` this line landed in the
|
|
178
|
+
* middle of the `published` lines rather than after them. */
|
|
179
|
+
if (failed.length) {
|
|
180
|
+
throw logged(`"publish" failed for ${failed.map(o => `[${o.target}] ${o.entry.package.name}`).join(', ')}`);
|
|
169
181
|
}
|
|
170
|
-
if (failed)
|
|
171
|
-
throw logged('"publish" failed');
|
|
172
182
|
},
|
|
173
183
|
};
|
|
174
184
|
});
|
|
@@ -265,25 +275,86 @@ function printPlan(entries, label) {
|
|
|
265
275
|
}
|
|
266
276
|
}
|
|
267
277
|
/**
|
|
268
|
-
* What one target actually did. Returns
|
|
278
|
+
* What one target actually did, printed as it is known. Returns the outcome of every package the
|
|
279
|
+
* *plan* said to publish.
|
|
269
280
|
*
|
|
270
|
-
* An `'error'` is only
|
|
281
|
+
* An `'error'` is only an outcome when the plan said `'publish'` for that package: an entry that
|
|
271
282
|
* was already an error before anything ran has been printed once by `printPlan`, and printing it
|
|
272
283
|
* again under "failed" would read as a push that was attempted and did not work.
|
|
273
284
|
*/
|
|
274
285
|
function printApplied(applied, plan, label) {
|
|
275
286
|
const prefix = colors.gray(`[${label}] `);
|
|
276
|
-
|
|
287
|
+
const outcomes = [];
|
|
277
288
|
for (const entry of applied) {
|
|
289
|
+
if (plan.find(e => e.package === entry.package)?.status !== 'publish')
|
|
290
|
+
continue;
|
|
278
291
|
if (entry.status === 'publish') {
|
|
279
292
|
console.log(colors.green('published'), prefix + colors.cyan(entry.package.name), entry.detail ?? entry.version);
|
|
280
293
|
}
|
|
281
|
-
else if (entry.status === 'error'
|
|
282
|
-
failed = true;
|
|
294
|
+
else if (entry.status === 'error') {
|
|
283
295
|
console.log(colors.red('failed'), prefix + colors.cyan(entry.package.name), colors.red(entry.reason ?? ''));
|
|
284
296
|
}
|
|
297
|
+
else
|
|
298
|
+
continue;
|
|
299
|
+
outcomes.push({ target: label, entry });
|
|
300
|
+
}
|
|
301
|
+
return outcomes;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* **One block at the end saying how it went**: how many went up, and every failure again with its
|
|
305
|
+
* reason. A target prints as it goes, so on a long release the failed line is hundreds of lines up,
|
|
306
|
+
* among the output of the build that failed - measured on `panates/syncbridge`, where the last
|
|
307
|
+
* thing in the log was `"publish" failed` and nothing said what.
|
|
308
|
+
*/
|
|
309
|
+
function printRecap(outcomes) {
|
|
310
|
+
if (!outcomes.length)
|
|
311
|
+
return;
|
|
312
|
+
const failed = outcomes.filter(o => o.entry.status === 'error');
|
|
313
|
+
const published = outcomes.length - failed.length;
|
|
314
|
+
console.log('\n' +
|
|
315
|
+
colors.bold('publish') +
|
|
316
|
+
' ' +
|
|
317
|
+
colors.green(`${published} published`) +
|
|
318
|
+
colors.gray(', ') +
|
|
319
|
+
(failed.length ? colors.red(`${failed.length} failed`) : colors.gray('0 failed')));
|
|
320
|
+
for (const { target, entry } of failed) {
|
|
321
|
+
const [first, ...rest] = (entry.reason ?? '').split('\n');
|
|
322
|
+
console.log(` ${colors.red('failed')} ${colors.gray(`[${target}]`)} ${colors.cyan(entry.package.name)} ${entry.version}`);
|
|
323
|
+
console.log(` ${colors.red(first ?? '')}`);
|
|
324
|
+
for (const line of rest)
|
|
325
|
+
console.log(colors.gray(` ${line}`));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The same outcome as a table on the job's summary page, when running in GitHub Actions - where a
|
|
330
|
+
* reader looks first, and where a failed release otherwise shows only "exit code 1".
|
|
331
|
+
*
|
|
332
|
+
* A no-op outside it (`GITHUB_STEP_SUMMARY` unset), and a summary that cannot be written is not a
|
|
333
|
+
* reason to fail a publish that worked.
|
|
334
|
+
*/
|
|
335
|
+
function writeStepSummary(outcomes) {
|
|
336
|
+
const file = process.env.GITHUB_STEP_SUMMARY;
|
|
337
|
+
if (!file || !outcomes.length)
|
|
338
|
+
return;
|
|
339
|
+
const cell = (text) => text.replace(/\|/g, '\\|').replace(/\n+/g, '<br>');
|
|
340
|
+
const rows = outcomes.map(({ target, entry }) => entry.status === 'error'
|
|
341
|
+
? `| ❌ failed | ${target} | \`${entry.package.name}\` | ${entry.version} | ${cell(entry.reason ?? '')} |`
|
|
342
|
+
: `| ✅ published | ${target} | \`${entry.package.name}\` | ${entry.version} | ${cell(entry.detail ?? '')} |`);
|
|
343
|
+
const failed = outcomes.filter(o => o.entry.status === 'error').length;
|
|
344
|
+
const markdown = [
|
|
345
|
+
`### rman publish - ${outcomes.length - failed} published, ${failed} failed`,
|
|
346
|
+
'',
|
|
347
|
+
'| Result | Target | Package | Version | Detail |',
|
|
348
|
+
'| --- | --- | --- | --- | --- |',
|
|
349
|
+
...rows,
|
|
350
|
+
'',
|
|
351
|
+
].join('\n');
|
|
352
|
+
try {
|
|
353
|
+
fs.appendFileSync(file, markdown + '\n');
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
/* The summary is a courtesy; the release itself is what the exit code reports. */
|
|
285
357
|
}
|
|
286
|
-
return failed;
|
|
287
358
|
}
|
|
288
359
|
/** The `logged` convention: printed here, so `runCli`'s catch does not print it a second time. */
|
|
289
360
|
/* On stderr, because under `--json` stdout is one JSON document: measured on opra's release, the
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.18.0';
|
|
@@ -41,6 +41,11 @@ export interface PublishTarget {
|
|
|
41
41
|
* Name a flag for the target when it genuinely belongs to one (`--docker-namespace`).
|
|
42
42
|
*/
|
|
43
43
|
readonly options?: Record<string, RmanConfig.CommandOption>;
|
|
44
|
+
/**
|
|
45
|
+
* Applied after every target without it. For a target whose artifact is built **from** what the
|
|
46
|
+
* others publish - an image whose `Dockerfile` runs `npm install` - and so has to wait for them.
|
|
47
|
+
*/
|
|
48
|
+
readonly publishesLast?: boolean;
|
|
44
49
|
/**
|
|
45
50
|
* The technologies this target is for, by `Platform.name` - `['node']` for `npm`. Absent means
|
|
46
51
|
* every platform, which is what a technology-agnostic target says: any package can carry a
|
package/index.d.ts
CHANGED
|
@@ -169,6 +169,7 @@ export { CiService } from './builtins/platforms/node/services/ci.service.js';
|
|
|
169
169
|
export { CleanService } from './builtins/platforms/node/services/clean.service.js';
|
|
170
170
|
export { PublishService } from './builtins/platforms/node/services/publish.service.js';
|
|
171
171
|
export { NodeVersionPlanService } from './builtins/platforms/node/services/version-plan.service.js';
|
|
172
|
+
export { copyAssets, type CopyAssetsOptions, DEFAULT_ASSET_PATTERNS, } from './builtins/platforms/node/utils/copy-assets.js';
|
|
172
173
|
export type { ParsedWorkspaceRange } from './builtins/platforms/node/utils/workspace-range.js';
|
|
173
174
|
export { NPM_TARGET, NpmPublishTarget } from './builtins/publish-targets/npm/npm-publish-target.js';
|
|
174
175
|
export { isCalendarVersion } from './utils/release-version.js';
|
package/index.js
CHANGED
|
@@ -135,6 +135,7 @@ export { CiService } from './builtins/platforms/node/services/ci.service.js';
|
|
|
135
135
|
export { CleanService } from './builtins/platforms/node/services/clean.service.js';
|
|
136
136
|
export { PublishService } from './builtins/platforms/node/services/publish.service.js';
|
|
137
137
|
export { NodeVersionPlanService } from './builtins/platforms/node/services/version-plan.service.js';
|
|
138
|
+
export { copyAssets, DEFAULT_ASSET_PATTERNS, } from './builtins/platforms/node/utils/copy-assets.js';
|
|
138
139
|
export { NPM_TARGET, NpmPublishTarget } from './builtins/publish-targets/npm/npm-publish-target.js';
|
|
139
140
|
export { isCalendarVersion } from './utils/release-version.js';
|
|
140
141
|
export { runBin } from './utils/run-bin.js';
|
|
@@ -72,6 +72,13 @@ export type CommandEntry = string | CustomCommand | RmanConfig.CommandRegisterFu
|
|
|
72
72
|
/** One entry of `.rmanrc "publishTargets"` - a target, or a **glob** naming `.js` modules that
|
|
73
73
|
* `export default` one. */
|
|
74
74
|
export type PublishTargetEntry = string | PublishTarget;
|
|
75
|
+
/**
|
|
76
|
+
* `.rmanrc "packageManager"`: one entry per technology, by `Platform.name`. Empty here - each
|
|
77
|
+
* platform that has a package manager to choose adds its own entry by declaration merging, the way
|
|
78
|
+
* `PublishTargetConfigs` collects the publish targets' blocks.
|
|
79
|
+
*/
|
|
80
|
+
export interface PackageManagers {
|
|
81
|
+
}
|
|
75
82
|
/**
|
|
76
83
|
* **The `.rmanrc` keys no command owns**, without `extends` - kept
|
|
77
84
|
* separate from `RmanConfig` only so the two halves stay legible. A key a *command* owns is
|
|
@@ -308,6 +315,11 @@ export interface RmanConfigKeys {
|
|
|
308
315
|
*/
|
|
309
316
|
vars?: Record<string, unknown>;
|
|
310
317
|
logLevel?: 'silent' | 'error' | 'info' | 'verbose';
|
|
318
|
+
/**
|
|
319
|
+
* Which package manager each technology uses, keyed by `Platform.name` - `{ node: 'pnpm' }`.
|
|
320
|
+
* Cascades per package. A technology with no choice to make (Cargo has `cargo`) adds no entry.
|
|
321
|
+
*/
|
|
322
|
+
packageManager?: PackageManagers;
|
|
311
323
|
allowBranch?: string | string[];
|
|
312
324
|
ignoreBranch?: string | string[];
|
|
313
325
|
/**
|
package/package.json
CHANGED
|
@@ -1,33 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rman",
|
|
3
3
|
"description": "Repository manager",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.18.0",
|
|
5
5
|
"author": "Panates",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"dependencies": {
|
|
8
|
+
"@jsopen/objects": "^2.3.4",
|
|
8
9
|
"@netlify/parse-npm-script": "^0.1.2",
|
|
9
10
|
"@xmldom/xmldom": "^0.9.12",
|
|
10
|
-
"@jsopen/objects": "^2.3.4",
|
|
11
11
|
"ansi-colors": "^4.1.3",
|
|
12
|
-
"cross-dirname": "^0.1.0",
|
|
13
12
|
"easy-table": "^1.2.0",
|
|
14
13
|
"envinfo": "^7.21.0",
|
|
15
14
|
"fast-glob": "^3.3.3",
|
|
16
|
-
"fast-tokenizer": "^1.10.
|
|
15
|
+
"fast-tokenizer": "^1.10.1",
|
|
17
16
|
"ini": "^7.0.0",
|
|
18
|
-
"is-ci": "^4.1.0",
|
|
19
17
|
"js-yaml": "^5.4.1",
|
|
20
18
|
"micromatch": "^4.0.8",
|
|
21
|
-
"npmlog": "^7.0.1",
|
|
22
19
|
"power-tasks": "^1.14.2",
|
|
23
|
-
"putil-merge": "^3.13.0",
|
|
24
|
-
"putil-varhelpers": "^1.7.0",
|
|
25
20
|
"semver": "^7.8.5",
|
|
26
21
|
"signal-exit": "^4.1.0",
|
|
27
|
-
"
|
|
28
|
-
"strip-color": "^0.1.0",
|
|
29
|
-
"tslib": "^2.8.1",
|
|
30
|
-
"yargs": "^18.1.0"
|
|
22
|
+
"yargs": "^18.2.0"
|
|
31
23
|
},
|
|
32
24
|
"type": "module",
|
|
33
25
|
"module": "./index.js",
|
package/utils/git.js
CHANGED
|
@@ -228,7 +228,7 @@ export class GitHelper {
|
|
|
228
228
|
async moveHistory(file) {
|
|
229
229
|
let stdout;
|
|
230
230
|
try {
|
|
231
|
-
({ stdout } = await execFileAsync('git', ['log', '--follow', '--format=%x00%H', '--name-status', '--', path.relative(this.cwd, file)], { cwd: this.cwd
|
|
231
|
+
({ stdout } = await execFileAsync('git', ['log', '--follow', '--format=%x00%H', '--name-status', '--', path.relative(this.cwd, file)], { cwd: this.cwd }));
|
|
232
232
|
}
|
|
233
233
|
catch {
|
|
234
234
|
return [];
|
|
@@ -263,7 +263,6 @@ export class GitHelper {
|
|
|
263
263
|
if (!renames) {
|
|
264
264
|
renames = execFileAsync('git', ['show', '-M', '--name-status', '--format=', sha], {
|
|
265
265
|
cwd: this.cwd,
|
|
266
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
267
266
|
}).then(({ stdout }) => stdout
|
|
268
267
|
.split(/\r?\n/)
|
|
269
268
|
.map(line => line.split('\t'))
|
|
@@ -275,7 +274,6 @@ export class GitHelper {
|
|
|
275
274
|
try {
|
|
276
275
|
const { stdout } = await execFileAsync('git', ['ls-tree', '-r', '--name-only', `${sha}^`, '--', `${fromDir}/`], {
|
|
277
276
|
cwd: this.cwd,
|
|
278
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
279
277
|
});
|
|
280
278
|
held = stdout.split(/\r?\n/).filter(Boolean);
|
|
281
279
|
}
|
|
@@ -436,7 +434,7 @@ export class GitHelper {
|
|
|
436
434
|
if (pathspec)
|
|
437
435
|
args.push('--', pathspec);
|
|
438
436
|
try {
|
|
439
|
-
const { stdout } = await execFileAsync('git', args, { cwd: this.cwd
|
|
437
|
+
const { stdout } = await execFileAsync('git', args, { cwd: this.cwd });
|
|
440
438
|
return stdout;
|
|
441
439
|
}
|
|
442
440
|
catch (e) {
|
|
@@ -450,7 +448,6 @@ export class GitHelper {
|
|
|
450
448
|
try {
|
|
451
449
|
const { stdout } = await execFileAsync('git', ['format-patch', '--root', '-o', outputDir, 'HEAD'], {
|
|
452
450
|
cwd: this.cwd,
|
|
453
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
454
451
|
});
|
|
455
452
|
return stdout.trim() ? stdout.trim().split(/\r?\n/) : [];
|
|
456
453
|
}
|
|
@@ -465,11 +462,20 @@ export class GitHelper {
|
|
|
465
462
|
if (!patchFiles.length)
|
|
466
463
|
return;
|
|
467
464
|
try {
|
|
468
|
-
await execFileAsync('git', ['am', '--3way', ...patchFiles], { cwd: this.cwd
|
|
465
|
+
await execFileAsync('git', ['am', '--3way', ...patchFiles], { cwd: this.cwd });
|
|
469
466
|
}
|
|
470
467
|
catch (e) {
|
|
471
468
|
throw new Error(`Unable to apply patches: ${e.message}`, { cause: e });
|
|
472
469
|
}
|
|
473
470
|
}
|
|
474
471
|
}
|
|
475
|
-
|
|
472
|
+
/* **One buffer for every git call, not one per call site.** Node's `execFile` defaults `maxBuffer`
|
|
473
|
+
* to 1 MiB and fails the call past it, and how much git prints depends on the repository, not on
|
|
474
|
+
* the command. Opting in call by call left the `git show --name-only` behind `listCommittedFiles`
|
|
475
|
+
* and `listCommits` on the default, so one vendored-library commit in `panates/formwave` - 28k
|
|
476
|
+
* files, 2.9 MB of names - failed `rman list` with "stdout maxBuffer length exceeded". */
|
|
477
|
+
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
478
|
+
const execFileP = promisify(execFile);
|
|
479
|
+
function execFileAsync(file, args, options) {
|
|
480
|
+
return execFileP(file, args, { maxBuffer: GIT_MAX_BUFFER, ...options });
|
|
481
|
+
}
|