@smoothbricks/cli 0.1.1 → 0.3.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 +161 -32
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +110 -10
- package/dist/devenv/index.d.ts +10 -0
- package/dist/devenv/index.d.ts.map +1 -0
- package/dist/devenv/index.js +27 -0
- package/dist/generate/index.d.ts +20 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/index.js +47 -0
- package/dist/lib/cli-package.d.ts +4 -0
- package/dist/lib/cli-package.d.ts.map +1 -0
- package/dist/lib/cli-package.js +15 -0
- package/dist/lib/run.d.ts +6 -0
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +32 -0
- package/dist/lib/workspace.d.ts +25 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +79 -17
- package/dist/monorepo/commit-msg.d.ts +8 -1
- package/dist/monorepo/commit-msg.d.ts.map +1 -1
- package/dist/monorepo/commit-msg.js +85 -7
- package/dist/monorepo/index.d.ts +19 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +104 -16
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +9 -0
- package/dist/monorepo/managed-files.js +1 -1
- package/dist/monorepo/nx-sync.d.ts +2 -1
- package/dist/monorepo/nx-sync.d.ts.map +1 -1
- package/dist/monorepo/nx-sync.js +8 -2
- package/dist/monorepo/package-hygiene.d.ts +6 -2
- package/dist/monorepo/package-hygiene.d.ts.map +1 -1
- package/dist/monorepo/package-hygiene.js +12 -4
- package/dist/monorepo/package-policy.d.ts +13 -4
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +208 -113
- package/dist/monorepo/packed-package.d.ts +3 -0
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +68 -36
- package/dist/monorepo/packs/index.d.ts +20 -1
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +238 -19
- package/dist/monorepo/publish-workflow.d.ts +0 -4
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +7 -15
- package/dist/monorepo/tool-validation.d.ts +22 -0
- package/dist/monorepo/tool-validation.d.ts.map +1 -0
- package/dist/monorepo/tool-validation.js +399 -0
- package/dist/nx/index.d.ts +25 -0
- package/dist/nx/index.d.ts.map +1 -0
- package/dist/nx/index.js +112 -0
- package/dist/release/bootstrap-npm-packages.d.ts +19 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
- package/dist/release/bootstrap-npm-packages.js +58 -0
- package/dist/release/candidates.d.ts +11 -0
- package/dist/release/candidates.d.ts.map +1 -0
- package/dist/release/candidates.js +181 -0
- package/dist/release/core.d.ts +4 -1
- package/dist/release/core.d.ts.map +1 -1
- package/dist/release/core.js +76 -9
- package/dist/release/github-release.d.ts +1 -0
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +3 -3
- package/dist/release/index.d.ts +32 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +488 -64
- package/dist/release/npm-auth.d.ts +0 -1
- package/dist/release/npm-auth.d.ts.map +1 -1
- package/dist/release/npm-auth.js +4 -16
- package/dist/release/orchestration.d.ts +11 -2
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +41 -3
- package/dist/release/publish-plan.js +1 -1
- package/managed/raw/git-format-staged.yml +54 -0
- package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
- package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
- package/managed/templates/github/workflows/ci.yml +4 -2
- package/package.json +15 -8
- package/src/cli.ts +153 -22
- package/src/devenv/index.test.ts +49 -0
- package/src/devenv/index.ts +38 -0
- package/src/generate/index.ts +92 -0
- package/src/lib/cli-package.ts +18 -0
- package/src/lib/run.ts +44 -0
- package/src/lib/workspace.ts +112 -17
- package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
- package/src/monorepo/commit-msg.test.ts +45 -0
- package/src/monorepo/commit-msg.ts +107 -7
- package/src/monorepo/index.ts +142 -16
- package/src/monorepo/lockfile.ts +9 -0
- package/src/monorepo/managed-files.ts +1 -1
- package/src/monorepo/nx-sync.ts +9 -2
- package/src/monorepo/package-hygiene.test.ts +34 -0
- package/src/monorepo/package-hygiene.ts +26 -4
- package/src/monorepo/package-policy.test.ts +1130 -0
- package/src/monorepo/package-policy.ts +237 -114
- package/src/monorepo/packed-package.ts +89 -37
- package/src/monorepo/packs/index.test.ts +189 -0
- package/src/monorepo/packs/index.ts +311 -20
- package/src/monorepo/publish-workflow.ts +10 -19
- package/src/monorepo/setup-test-tracing.test.ts +120 -0
- package/src/monorepo/tool-validation.test.ts +261 -0
- package/src/monorepo/tool-validation.ts +462 -0
- package/src/nx/index.test.ts +73 -0
- package/src/nx/index.ts +139 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
- package/src/release/__tests__/candidates.test.ts +262 -0
- package/src/release/__tests__/core-properties.test.ts +6 -6
- package/src/release/__tests__/core-scenarios.test.ts +27 -20
- package/src/release/__tests__/core.test.ts +52 -5
- package/src/release/__tests__/fixture-repo.test.ts +90 -31
- package/src/release/__tests__/github-release.test.ts +22 -24
- package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
- package/src/release/__tests__/npm-auth.test.ts +12 -21
- package/src/release/__tests__/orchestration.test.ts +142 -12
- package/src/release/__tests__/publish-plan.test.ts +9 -3
- package/src/release/__tests__/retag-unpublished.test.ts +10 -10
- package/src/release/__tests__/trust-publisher.test.ts +165 -0
- package/src/release/bootstrap-npm-packages.ts +88 -0
- package/src/release/candidates.ts +225 -0
- package/src/release/core.ts +96 -10
- package/src/release/github-release.ts +3 -3
- package/src/release/index.ts +658 -74
- package/src/release/npm-auth.ts +4 -33
- package/src/release/orchestration.ts +60 -5
- package/src/release/publish-plan.ts +1 -1
- package/dist/nx-version-actions.cjs +0 -25
- package/dist/nx-version-actions.d.cts +0 -6
- package/dist/nx-version-actions.d.cts.map +0 -1
- package/managed/raw/.git-format-staged.yml +0 -47
- package/src/nx-version-actions.cts +0 -36
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import type { DevenvCommandShell } from './index.js';
|
|
3
|
+
import { reloadDevenv, updateDevenv, updateNixpkgsOverlay } from './index.js';
|
|
4
|
+
|
|
5
|
+
describe('devenv commands', () => {
|
|
6
|
+
it('runs devenv update in tooling/direnv', async () => {
|
|
7
|
+
const shell = new RecordingShell();
|
|
8
|
+
|
|
9
|
+
await updateDevenv('/repo', shell);
|
|
10
|
+
|
|
11
|
+
expect(shell.commands).toEqual([{ command: 'devenv', args: ['update'], cwd: '/repo/tooling/direnv' }]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('clears cached devenv directories before reloading direnv', async () => {
|
|
15
|
+
const shell = new RecordingShell();
|
|
16
|
+
|
|
17
|
+
await reloadDevenv('/repo', shell);
|
|
18
|
+
|
|
19
|
+
expect(shell.removals).toEqual(['/repo/tooling/direnv/.direnv', '/repo/tooling/direnv/.devenv']);
|
|
20
|
+
expect(shell.commands).toEqual([{ command: 'direnv', args: ['reload'], cwd: '/repo/tooling/direnv' }]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('runs nvfetcher through nix shell in the overlay directory', async () => {
|
|
24
|
+
const shell = new RecordingShell();
|
|
25
|
+
|
|
26
|
+
await updateNixpkgsOverlay('/repo', shell);
|
|
27
|
+
|
|
28
|
+
expect(shell.commands).toEqual([
|
|
29
|
+
{
|
|
30
|
+
command: 'nix',
|
|
31
|
+
args: ['shell', 'nixpkgs#nvfetcher', '-c', 'nvfetcher', '-o', '_sources'],
|
|
32
|
+
cwd: '/repo/tooling/direnv/nixpkgs-overlay',
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
class RecordingShell implements DevenvCommandShell {
|
|
39
|
+
readonly commands: { command: string; args: string[]; cwd: string }[] = [];
|
|
40
|
+
readonly removals: string[] = [];
|
|
41
|
+
|
|
42
|
+
async run(command: string, args: string[], cwd: string): Promise<void> {
|
|
43
|
+
this.commands.push({ command, args, cwd });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async remove(path: string): Promise<void> {
|
|
47
|
+
this.removals.push(path);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { run } from '../lib/run.js';
|
|
4
|
+
|
|
5
|
+
export interface DevenvCommandShell {
|
|
6
|
+
run(command: string, args: string[], cwd: string): Promise<void>;
|
|
7
|
+
remove(path: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const defaultShell: DevenvCommandShell = {
|
|
11
|
+
run,
|
|
12
|
+
async remove(path) {
|
|
13
|
+
await rm(path, { recursive: true, force: true });
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function updateDevenv(root: string, shell: DevenvCommandShell = defaultShell): Promise<void> {
|
|
18
|
+
await shell.run('devenv', ['update'], direnvRoot(root));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function reloadDevenv(root: string, shell: DevenvCommandShell = defaultShell): Promise<void> {
|
|
22
|
+
const cwd = direnvRoot(root);
|
|
23
|
+
await shell.remove(join(cwd, '.direnv'));
|
|
24
|
+
await shell.remove(join(cwd, '.devenv'));
|
|
25
|
+
await shell.run('direnv', ['reload'], cwd);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function updateNixpkgsOverlay(root: string, shell: DevenvCommandShell = defaultShell): Promise<void> {
|
|
29
|
+
await shell.run('nix', ['shell', 'nixpkgs#nvfetcher', '-c', 'nvfetcher', '-o', '_sources'], nixpkgsOverlayRoot(root));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function direnvRoot(root: string): string {
|
|
33
|
+
return join(root, 'tooling', 'direnv');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function nixpkgsOverlayRoot(root: string): string {
|
|
37
|
+
return join(direnvRoot(root), 'nixpkgs-overlay');
|
|
38
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { run } from '../lib/run.js';
|
|
2
|
+
|
|
3
|
+
const plugin = '@smoothbricks/nx-plugin';
|
|
4
|
+
|
|
5
|
+
// Variant registry — drives both CLI subcommands and dispatch.
|
|
6
|
+
//
|
|
7
|
+
// Each entry maps a `smoo g <variant>` subcommand to an Nx generator in
|
|
8
|
+
// @smoothbricks/nx-plugin. cli.ts iterates this record to register
|
|
9
|
+
// commander subcommands, so adding a variant here wires it end-to-end.
|
|
10
|
+
//
|
|
11
|
+
// Generator implementations:
|
|
12
|
+
// create-package packages/nx-plugin/src/generators/create-package/generator.ts
|
|
13
|
+
// make-public packages/nx-plugin/src/generators/make-public/generator.ts
|
|
14
|
+
//
|
|
15
|
+
// To add a new variant:
|
|
16
|
+
// 1. Add/extend a generator in packages/nx-plugin/src/generators/
|
|
17
|
+
// 2. Register it in packages/nx-plugin/generators.json
|
|
18
|
+
// 3. Add a variants entry below
|
|
19
|
+
|
|
20
|
+
export interface GenerateVariant {
|
|
21
|
+
/** Nx generator name inside @smoothbricks/nx-plugin (e.g. 'create-package'). */
|
|
22
|
+
generator: string;
|
|
23
|
+
/** One-line description shown in `smoo g --help`. */
|
|
24
|
+
description: string;
|
|
25
|
+
/** Build the generator-specific args from the positional <name>. */
|
|
26
|
+
args: (name: string) => string[];
|
|
27
|
+
/** Extra CLI flags beyond the universal --dry-run. */
|
|
28
|
+
options?: readonly VariantOption[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface VariantOption {
|
|
32
|
+
/** Commander flag syntax (e.g. '--public'). */
|
|
33
|
+
flag: string;
|
|
34
|
+
/** Help text for this flag. */
|
|
35
|
+
description: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const variants: Record<string, GenerateVariant> = {
|
|
39
|
+
'ts-lib': {
|
|
40
|
+
generator: 'create-package',
|
|
41
|
+
description: 'Create a TypeScript library package',
|
|
42
|
+
args: (name) => ['--name', name, '--variant', 'ts-lib'],
|
|
43
|
+
options: [{ flag: '--public', description: 'configure for npm publication' }],
|
|
44
|
+
},
|
|
45
|
+
'ts-zig': {
|
|
46
|
+
generator: 'create-package',
|
|
47
|
+
description: 'Create a TypeScript + Zig/WASM hybrid package',
|
|
48
|
+
args: (name) => ['--name', name, '--variant', 'ts-zig'],
|
|
49
|
+
options: [{ flag: '--public', description: 'configure for npm publication' }],
|
|
50
|
+
},
|
|
51
|
+
'make-public': {
|
|
52
|
+
generator: 'make-public',
|
|
53
|
+
description: 'Promote a private package to npm publication',
|
|
54
|
+
args: (name) => ['--project', name],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Dispatch
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export async function generate(
|
|
63
|
+
root: string,
|
|
64
|
+
variantName: string,
|
|
65
|
+
name: string,
|
|
66
|
+
options: Record<string, unknown>,
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
const variant = variants[variantName];
|
|
69
|
+
if (!variant) {
|
|
70
|
+
const known = Object.keys(variants).join(', ');
|
|
71
|
+
throw new Error(`Unknown generator variant "${variantName}". Available: ${known}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const args = ['g', `${plugin}:${variant.generator}`, ...variant.args(name)];
|
|
75
|
+
for (const opt of variant.options ?? []) {
|
|
76
|
+
const key = opt.flag.replace(/^--/, '');
|
|
77
|
+
if (options[key]) {
|
|
78
|
+
args.push(opt.flag);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (options.dryRun) {
|
|
82
|
+
args.push('--dry-run');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await run('nx', args, root);
|
|
86
|
+
|
|
87
|
+
// Sync TypeScript project references so the new package is immediately
|
|
88
|
+
// visible in the build graph without a manual `nx sync` step.
|
|
89
|
+
if (!options.dryRun) {
|
|
90
|
+
await run('nx', ['sync'], root);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { readJsonObject, stringProperty } from './json.js';
|
|
3
|
+
|
|
4
|
+
export const smoothBricksCodebasePackageName = '@smoothbricks/codebase';
|
|
5
|
+
export const cliPackageVersion = readCliPackageVersion();
|
|
6
|
+
|
|
7
|
+
function readCliPackageVersion(): string {
|
|
8
|
+
const pkg = readJsonObject(fileURLToPath(new URL('../../package.json', import.meta.url)));
|
|
9
|
+
const version = pkg ? stringProperty(pkg, 'version') : null;
|
|
10
|
+
if (!version) {
|
|
11
|
+
throw new Error('Unable to read @smoothbricks/cli package version.');
|
|
12
|
+
}
|
|
13
|
+
return version;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isSmoothBricksCodebasePackageName(name: string | undefined): boolean {
|
|
17
|
+
return name === smoothBricksCodebasePackageName;
|
|
18
|
+
}
|
package/src/lib/run.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -26,6 +27,49 @@ export async function runStatus(
|
|
|
26
27
|
return result.exitCode;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export async function runInteractiveStatus(
|
|
31
|
+
command: string,
|
|
32
|
+
args: string[],
|
|
33
|
+
cwd: string,
|
|
34
|
+
env?: Record<string, string>,
|
|
35
|
+
): Promise<number> {
|
|
36
|
+
const invocation = resolveCommandInvocation(cwd, command, args);
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
39
|
+
cwd,
|
|
40
|
+
env: env ? mergeEnv(env) : process.env,
|
|
41
|
+
stdio: 'inherit',
|
|
42
|
+
});
|
|
43
|
+
child.on('error', reject);
|
|
44
|
+
child.on('close', (code, signal) => {
|
|
45
|
+
if (signal) {
|
|
46
|
+
reject(new Error(`${command} ${args.join(' ')} terminated by signal ${signal}`));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
resolve(code ?? 1);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runResult(
|
|
55
|
+
command: string,
|
|
56
|
+
args: string[],
|
|
57
|
+
cwd: string,
|
|
58
|
+
env?: Record<string, string>,
|
|
59
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
60
|
+
const invocation = resolveCommandInvocation(cwd, command, args);
|
|
61
|
+
let shell = $`${invocation.command} ${invocation.args}`.cwd(cwd).nothrow().quiet();
|
|
62
|
+
if (env) {
|
|
63
|
+
shell = shell.env(mergeEnv(env));
|
|
64
|
+
}
|
|
65
|
+
const result = await shell;
|
|
66
|
+
return {
|
|
67
|
+
exitCode: result.exitCode,
|
|
68
|
+
stdout: decode(result.stdout),
|
|
69
|
+
stderr: decode(result.stderr),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
29
73
|
function mergeEnv(env: Record<string, string>): Record<string, string> {
|
|
30
74
|
const merged: Record<string, string> = {};
|
|
31
75
|
for (const [key, value] of Object.entries(process.env)) {
|
package/src/lib/workspace.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
import { isSmoothBricksCodebasePackageName } from './cli-package.js';
|
|
3
4
|
import { hasOwn, hasOwnString, isRecord, readJson, readJsonObject, stringProperty } from './json.js';
|
|
4
5
|
|
|
5
6
|
export interface PackageInfo {
|
|
6
7
|
name: string;
|
|
8
|
+
projectName: string;
|
|
7
9
|
version: string;
|
|
8
10
|
private: boolean;
|
|
9
11
|
tags: string[];
|
|
@@ -12,12 +14,37 @@ export interface PackageInfo {
|
|
|
12
14
|
json: Record<string, unknown>;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
export interface WorkspacePackageManifest {
|
|
18
|
+
name: string;
|
|
19
|
+
projectName: string;
|
|
20
|
+
private: boolean;
|
|
21
|
+
path: string;
|
|
22
|
+
packageJsonPath: string;
|
|
23
|
+
json: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PackageJson extends Record<string, unknown> {
|
|
27
|
+
name?: string;
|
|
28
|
+
version?: string;
|
|
29
|
+
private?: boolean;
|
|
30
|
+
workspaces?: unknown;
|
|
31
|
+
devDependencies?: unknown;
|
|
32
|
+
dependencies?: unknown;
|
|
33
|
+
nx?: unknown;
|
|
34
|
+
repository?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
export interface RepositoryInfo {
|
|
16
38
|
type: string;
|
|
17
39
|
url: string;
|
|
18
40
|
}
|
|
19
41
|
|
|
20
|
-
export const workspaceDependencyFields = [
|
|
42
|
+
export const workspaceDependencyFields = [
|
|
43
|
+
'dependencies',
|
|
44
|
+
'devDependencies',
|
|
45
|
+
'peerDependencies',
|
|
46
|
+
'optionalDependencies',
|
|
47
|
+
] as const;
|
|
21
48
|
|
|
22
49
|
export function listPublicPackages(root: string): PackageInfo[] {
|
|
23
50
|
return getWorkspacePackages(root).filter((pkg) => !pkg.private && pkg.tags.includes('npm:public'));
|
|
@@ -44,6 +71,15 @@ export function rootRepositoryInfo(root: string): RepositoryInfo | null {
|
|
|
44
71
|
return rootPackage ? repositoryInfo(rootPackage) : null;
|
|
45
72
|
}
|
|
46
73
|
|
|
74
|
+
export function rootPackageName(root: string): string | null {
|
|
75
|
+
const rootPackage = readPackageJsonObject(join(root, 'package.json'));
|
|
76
|
+
return rootPackage ? stringProperty(rootPackage, 'name') : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isSmoothBricksCodebase(root: string): boolean {
|
|
80
|
+
return isSmoothBricksCodebasePackageName(rootPackageName(root) ?? undefined);
|
|
81
|
+
}
|
|
82
|
+
|
|
47
83
|
export function packageRepositoryInfo(pkg: PackageInfo): RepositoryInfo | null {
|
|
48
84
|
return repositoryInfo(pkg.json);
|
|
49
85
|
}
|
|
@@ -61,34 +97,66 @@ export function getWorkspacePackages(root: string): PackageInfo[] {
|
|
|
61
97
|
return getWorkspacePackagesForPatterns(root, getWorkspacePatternsFromPackageJson(rootPackage.json));
|
|
62
98
|
}
|
|
63
99
|
|
|
100
|
+
export function getWorkspacePackageManifests(root: string): WorkspacePackageManifest[] {
|
|
101
|
+
const rootPackage = readPackageJsonObject(join(root, 'package.json'));
|
|
102
|
+
if (!rootPackage) {
|
|
103
|
+
throw new Error('package.json not found or invalid');
|
|
104
|
+
}
|
|
105
|
+
return getWorkspacePackageManifestsForPatterns(root, getWorkspacePatternsFromPackageJson(rootPackage));
|
|
106
|
+
}
|
|
107
|
+
|
|
64
108
|
function getWorkspacePackagesForPatterns(root: string, workspacePatterns: string[]): PackageInfo[] {
|
|
65
109
|
const packages: PackageInfo[] = [];
|
|
110
|
+
for (const pkgPath of listWorkspacePackageJsonPaths(root, workspacePatterns)) {
|
|
111
|
+
const pkg = readPackageJson(pkgPath);
|
|
112
|
+
if (!pkg?.name || !pkg.version) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
packages.push({
|
|
116
|
+
name: pkg.name,
|
|
117
|
+
projectName: pkg.projectName,
|
|
118
|
+
version: pkg.version,
|
|
119
|
+
private: pkg.private,
|
|
120
|
+
tags: pkg.tags,
|
|
121
|
+
path: relative(root, dirname(pkgPath)),
|
|
122
|
+
packageJsonPath: pkg.packageJsonPath,
|
|
123
|
+
json: pkg.json,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getWorkspacePackageManifestsForPatterns(
|
|
130
|
+
root: string,
|
|
131
|
+
workspacePatterns: string[],
|
|
132
|
+
): WorkspacePackageManifest[] {
|
|
133
|
+
const manifests: WorkspacePackageManifest[] = [];
|
|
134
|
+
for (const pkgPath of listWorkspacePackageJsonPaths(root, workspacePatterns)) {
|
|
135
|
+
const pkg = readWorkspacePackageManifest(pkgPath);
|
|
136
|
+
if (pkg) {
|
|
137
|
+
manifests.push(pkg);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return manifests.sort((a, b) => a.name.localeCompare(b.name));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function listWorkspacePackageJsonPaths(root: string, workspacePatterns: string[]): string[] {
|
|
144
|
+
const paths: string[] = [];
|
|
66
145
|
for (const pattern of workspacePatterns) {
|
|
67
146
|
if (!pattern.endsWith('/*')) {
|
|
147
|
+
paths.push(join(root, pattern, 'package.json'));
|
|
68
148
|
continue;
|
|
69
149
|
}
|
|
150
|
+
|
|
70
151
|
const parent = join(root, pattern.slice(0, -2));
|
|
71
152
|
if (!statSync(parent, { throwIfNoEntry: false })?.isDirectory()) {
|
|
72
153
|
continue;
|
|
73
154
|
}
|
|
74
155
|
for (const entry of readdirSync(parent)) {
|
|
75
|
-
|
|
76
|
-
const pkg = readPackageJson(pkgPath);
|
|
77
|
-
if (!pkg?.name || !pkg.version) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
packages.push({
|
|
81
|
-
name: pkg.name,
|
|
82
|
-
version: pkg.version,
|
|
83
|
-
private: pkg.private,
|
|
84
|
-
tags: pkg.tags,
|
|
85
|
-
path: relative(root, dirname(pkgPath)),
|
|
86
|
-
packageJsonPath: pkg.packageJsonPath,
|
|
87
|
-
json: pkg.json,
|
|
88
|
-
});
|
|
156
|
+
paths.push(join(parent, entry, 'package.json'));
|
|
89
157
|
}
|
|
90
158
|
}
|
|
91
|
-
return
|
|
159
|
+
return paths;
|
|
92
160
|
}
|
|
93
161
|
|
|
94
162
|
export function listPackageJsonRecords(root: string): PackageInfo[] {
|
|
@@ -122,7 +190,7 @@ function getWorkspacePatternsFromPackageJson(pkg: Record<string, unknown>): stri
|
|
|
122
190
|
}
|
|
123
191
|
|
|
124
192
|
export function readPackageJson(path: string): PackageInfo | null {
|
|
125
|
-
const parsed =
|
|
193
|
+
const parsed = readPackageJsonObject(path);
|
|
126
194
|
if (!isRecord(parsed) || !hasOwnString(parsed, 'name') || !hasOwnString(parsed, 'version')) {
|
|
127
195
|
return null;
|
|
128
196
|
}
|
|
@@ -130,6 +198,7 @@ export function readPackageJson(path: string): PackageInfo | null {
|
|
|
130
198
|
const tags = getNxTags(parsed);
|
|
131
199
|
return {
|
|
132
200
|
name: parsed.name,
|
|
201
|
+
projectName: packageNxProjectName(parsed),
|
|
133
202
|
version: parsed.version,
|
|
134
203
|
private: privateValue,
|
|
135
204
|
tags,
|
|
@@ -139,6 +208,32 @@ export function readPackageJson(path: string): PackageInfo | null {
|
|
|
139
208
|
};
|
|
140
209
|
}
|
|
141
210
|
|
|
211
|
+
export function readWorkspacePackageManifest(path: string): WorkspacePackageManifest | null {
|
|
212
|
+
const parsed = readPackageJsonObject(path);
|
|
213
|
+
if (!isRecord(parsed) || !hasOwnString(parsed, 'name')) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const privateValue = hasOwn(parsed, 'private') && typeof parsed.private === 'boolean' ? parsed.private : false;
|
|
217
|
+
return {
|
|
218
|
+
name: parsed.name,
|
|
219
|
+
projectName: packageNxProjectName(parsed),
|
|
220
|
+
private: privateValue,
|
|
221
|
+
path: dirname(path),
|
|
222
|
+
packageJsonPath: path,
|
|
223
|
+
json: parsed,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function packageNxProjectName(pkg: Record<string, unknown>): string {
|
|
228
|
+
const nx = isRecord(pkg.nx) ? pkg.nx : null;
|
|
229
|
+
return (nx ? stringProperty(nx, 'name') : null) ?? stringProperty(pkg, 'name') ?? '';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function readPackageJsonObject(path: string): PackageJson | null {
|
|
233
|
+
const parsed = readJsonObject(path);
|
|
234
|
+
return parsed;
|
|
235
|
+
}
|
|
236
|
+
|
|
142
237
|
export function getNxTags(pkg: Record<string, unknown>): string[] {
|
|
143
238
|
if (!hasOwn(pkg, 'nx') || !isRecord(pkg.nx) || !hasOwn(pkg.nx, 'tags') || !Array.isArray(pkg.nx.tags)) {
|
|
144
239
|
return [];
|
|
@@ -20,6 +20,14 @@ describe('publish workflow definition', () => {
|
|
|
20
20
|
);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
it('does not wire npm token secrets into repair or publish steps', () => {
|
|
24
|
+
const rendered = renderPublishWorkflowYaml({ repoName: '@smoothbricks/codebase' });
|
|
25
|
+
|
|
26
|
+
expect(rendered).not.toContain('NODE_AUTH_TOKEN');
|
|
27
|
+
expect(rendered).not.toContain('secrets.NPM_TOKEN');
|
|
28
|
+
expect(rendered).toContain('packages must already exist on npm and use trusted publishing/OIDC');
|
|
29
|
+
});
|
|
30
|
+
|
|
23
31
|
it('bootstraps the self-hosted CLI before release commands only for the SmoothBricks repo', async () => {
|
|
24
32
|
const smoothbricks = await publishWorkflowScenario({
|
|
25
33
|
repoName: '@smoothbricks/codebase',
|
|
@@ -79,16 +87,16 @@ describe('publish workflow definition', () => {
|
|
|
79
87
|
],
|
|
80
88
|
bump: 'patch',
|
|
81
89
|
dryRun: false,
|
|
82
|
-
version: { mode: 'new', projects: ['
|
|
90
|
+
version: { mode: 'new', projects: ['a', 'b'] },
|
|
83
91
|
});
|
|
84
92
|
|
|
85
93
|
const outcome = await scenario.run();
|
|
86
94
|
|
|
87
95
|
expect(outcome.validation).toEqual({
|
|
88
96
|
checks: 1,
|
|
89
|
-
builds: ['
|
|
90
|
-
lints: ['
|
|
91
|
-
tests: ['
|
|
97
|
+
builds: ['a', 'b'],
|
|
98
|
+
lints: ['a', 'b'],
|
|
99
|
+
tests: ['a', 'b'],
|
|
92
100
|
validates: 1,
|
|
93
101
|
});
|
|
94
102
|
expect(outcome.publishSawValidatedRelease).toBe(true);
|
|
@@ -102,7 +110,7 @@ describe('publish workflow definition', () => {
|
|
|
102
110
|
current: [{ tag: '@scope/a@1.2.0', npmMissing: true, githubMissing: true }],
|
|
103
111
|
bump: 'prerelease',
|
|
104
112
|
dryRun: true,
|
|
105
|
-
version: { mode: 'new', projects: ['
|
|
113
|
+
version: { mode: 'new', projects: ['a'] },
|
|
106
114
|
});
|
|
107
115
|
|
|
108
116
|
const outcome = await scenario.run();
|
|
@@ -112,9 +120,9 @@ describe('publish workflow definition', () => {
|
|
|
112
120
|
expect(outcome.repairBuildArtifacts).toEqual([]);
|
|
113
121
|
expect(outcome.validation).toEqual({
|
|
114
122
|
checks: 1,
|
|
115
|
-
builds: ['
|
|
116
|
-
lints: ['
|
|
117
|
-
tests: ['
|
|
123
|
+
builds: ['a'],
|
|
124
|
+
lints: ['a'],
|
|
125
|
+
tests: ['a'],
|
|
118
126
|
validates: 1,
|
|
119
127
|
});
|
|
120
128
|
expect(outcome.publishRan).toBe(true);
|
|
@@ -164,7 +172,6 @@ function publishWorkflowScenario(config: WorkflowScenarioConfig): { run(): Promi
|
|
|
164
172
|
const state = new WorkflowScenarioState(config);
|
|
165
173
|
await runPublishWorkflow(definePublishWorkflow({ repoName: config.repoName }), {
|
|
166
174
|
inputs: { bump: config.bump, dryRun: config.dryRun },
|
|
167
|
-
nodeAuthToken: true,
|
|
168
175
|
callbacks: state.callbacks(),
|
|
169
176
|
});
|
|
170
177
|
return state.outcome();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { formatCommitMessage, validateCommitMessage } from './commit-msg.js';
|
|
3
|
+
|
|
4
|
+
describe('commit message validation', () => {
|
|
5
|
+
it('accepts Nx conventional commit types and configured scopes', () => {
|
|
6
|
+
expect(validateCommitMessage('types(cli): expose public API\n', { validScopes: new Set(['cli']) })).toBeNull();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('rejects scopes outside configured Nx names', () => {
|
|
10
|
+
expect(
|
|
11
|
+
validateCommitMessage('fix(@smoothbricks/cli): repair release\n', { validScopes: new Set(['cli']) }),
|
|
12
|
+
).toContain('Invalid conventional commit scope');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('commit message formatting', () => {
|
|
17
|
+
it('wraps prose and preserves markdown blocks', () => {
|
|
18
|
+
const message = formatCommitMessage(
|
|
19
|
+
[
|
|
20
|
+
'fix(cli): wrap commit bodies ',
|
|
21
|
+
'',
|
|
22
|
+
'This paragraph is intentionally long enough to be wrapped by the injected formatter callback while preserving non-prose markdown sections.',
|
|
23
|
+
'',
|
|
24
|
+
'```',
|
|
25
|
+
'long log errors should stay exactly as they are because this is a fenced block',
|
|
26
|
+
'```',
|
|
27
|
+
'',
|
|
28
|
+
'> quoted markdown should also stay as a single untouched line even when it is long enough to otherwise need wrapping',
|
|
29
|
+
].join('\n'),
|
|
30
|
+
{ wrapBody: (paragraph) => paragraph.replace(' while ', '\nwhile ') },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(message).toBe(`fix(cli): wrap commit bodies
|
|
34
|
+
|
|
35
|
+
This paragraph is intentionally long enough to be wrapped by the injected formatter callback
|
|
36
|
+
while preserving non-prose markdown sections.
|
|
37
|
+
|
|
38
|
+
\`\`\`
|
|
39
|
+
long log errors should stay exactly as they are because this is a fenced block
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
> quoted markdown should also stay as a single untouched line even when it is long enough to otherwise need wrapping
|
|
43
|
+
`);
|
|
44
|
+
});
|
|
45
|
+
});
|