@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,127 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
type BootstrapNpmPackagesShell,
|
|
4
|
+
bootstrapNpmPackages,
|
|
5
|
+
NPM_BOOTSTRAP_DIST_TAG,
|
|
6
|
+
NPM_BOOTSTRAP_VERSION,
|
|
7
|
+
} from '../bootstrap-npm-packages.js';
|
|
8
|
+
import type { ReleasePackageInfo } from '../core.js';
|
|
9
|
+
|
|
10
|
+
const stable: ReleasePackageInfo = {
|
|
11
|
+
name: '@scope/stable',
|
|
12
|
+
projectName: 'stable',
|
|
13
|
+
path: 'packages/stable',
|
|
14
|
+
version: '1.2.3',
|
|
15
|
+
};
|
|
16
|
+
const missing: ReleasePackageInfo = {
|
|
17
|
+
name: '@scope/missing',
|
|
18
|
+
projectName: 'missing',
|
|
19
|
+
path: 'packages/missing',
|
|
20
|
+
version: '2.0.0',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('bootstrap npm packages', () => {
|
|
24
|
+
it('publishes placeholders only for selected packages missing from npm', async () => {
|
|
25
|
+
const shell = new RecordingBootstrapShell({
|
|
26
|
+
packages: [stable, missing],
|
|
27
|
+
existing: [stable.name],
|
|
28
|
+
otps: ['123456'],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const bootstrapped = await bootstrapNpmPackages(shell, {
|
|
32
|
+
dryRun: false,
|
|
33
|
+
skipLogin: false,
|
|
34
|
+
packages: [missing.name],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(bootstrapped.map((pkg) => pkg.name)).toEqual([missing.name]);
|
|
38
|
+
expect(shell.logins).toBe(1);
|
|
39
|
+
expect(shell.published).toEqual([{ name: missing.name, otp: '123456' }]);
|
|
40
|
+
expect(shell.logs.join('\n')).toContain(NPM_BOOTSTRAP_VERSION);
|
|
41
|
+
expect(shell.logs.join('\n')).toContain(NPM_BOOTSTRAP_DIST_TAG);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('dry-run reports missing packages without login or publish', async () => {
|
|
45
|
+
const shell = new RecordingBootstrapShell({ packages: [stable, missing], existing: [stable.name] });
|
|
46
|
+
|
|
47
|
+
const bootstrapped = await bootstrapNpmPackages(shell, { dryRun: true, skipLogin: false, packages: [] });
|
|
48
|
+
|
|
49
|
+
expect(bootstrapped.map((pkg) => pkg.name)).toEqual([missing.name]);
|
|
50
|
+
expect(shell.logins).toBe(0);
|
|
51
|
+
expect(shell.published).toEqual([]);
|
|
52
|
+
expect(shell.logs).toContain(`${stable.name}: already exists on npm; skipping placeholder bootstrap.`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('supports skipping npm login when an existing session is already authenticated', async () => {
|
|
56
|
+
const shell = new RecordingBootstrapShell({ packages: [missing], existing: [], otps: ['654321'] });
|
|
57
|
+
|
|
58
|
+
await bootstrapNpmPackages(shell, { dryRun: false, skipLogin: true, packages: [] });
|
|
59
|
+
|
|
60
|
+
expect(shell.logins).toBe(0);
|
|
61
|
+
expect(shell.published).toEqual([{ name: missing.name, otp: '654321' }]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('passes explicit OTP to placeholder publishes without prompting', async () => {
|
|
65
|
+
const shell = new RecordingBootstrapShell({ packages: [missing], existing: [] });
|
|
66
|
+
|
|
67
|
+
await bootstrapNpmPackages(shell, { dryRun: false, skipLogin: true, packages: [], otp: '111222' });
|
|
68
|
+
|
|
69
|
+
expect(shell.published).toEqual([{ name: missing.name, otp: '111222' }]);
|
|
70
|
+
expect(shell.prompts).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('rejects unknown package selections before npm login', async () => {
|
|
74
|
+
const shell = new RecordingBootstrapShell({ packages: [stable], existing: [] });
|
|
75
|
+
|
|
76
|
+
await expect(
|
|
77
|
+
bootstrapNpmPackages(shell, { dryRun: false, skipLogin: false, packages: ['@scope/unknown'] }),
|
|
78
|
+
).rejects.toThrow('Unknown owned release package selection: @scope/unknown');
|
|
79
|
+
expect(shell.logins).toBe(0);
|
|
80
|
+
expect(shell.published).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
class RecordingBootstrapShell implements BootstrapNpmPackagesShell<ReleasePackageInfo> {
|
|
85
|
+
readonly logs: string[] = [];
|
|
86
|
+
readonly published: Array<{ name: string; otp: string }> = [];
|
|
87
|
+
readonly prompts: string[] = [];
|
|
88
|
+
logins = 0;
|
|
89
|
+
private readonly packages: ReleasePackageInfo[];
|
|
90
|
+
private readonly existing: Set<string>;
|
|
91
|
+
private readonly otps: string[];
|
|
92
|
+
|
|
93
|
+
constructor(options: { packages: ReleasePackageInfo[]; existing: string[]; otps?: string[] }) {
|
|
94
|
+
this.packages = options.packages;
|
|
95
|
+
this.existing = new Set(options.existing);
|
|
96
|
+
this.otps = [...(options.otps ?? [])];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
listReleasePackages(): ReleasePackageInfo[] {
|
|
100
|
+
return this.packages;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async packageExists(name: string): Promise<boolean> {
|
|
104
|
+
return this.existing.has(name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async login(): Promise<void> {
|
|
108
|
+
this.logins += 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async publishPlaceholder(pkg: ReleasePackageInfo, env?: Record<string, string>): Promise<void> {
|
|
112
|
+
this.published.push({ name: pkg.name, otp: env?.NPM_CONFIG_OTP ?? '' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async promptOtp(packageName: string): Promise<string> {
|
|
116
|
+
this.prompts.push(packageName);
|
|
117
|
+
const otp = this.otps.shift();
|
|
118
|
+
if (!otp) {
|
|
119
|
+
throw new Error(`unexpected OTP prompt for ${packageName}`);
|
|
120
|
+
}
|
|
121
|
+
return otp;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
log(message: string): void {
|
|
125
|
+
this.logs.push(message);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { $ } from 'bun';
|
|
5
|
+
import { isRecord } from '../../lib/json.js';
|
|
6
|
+
import { type AutoReleaseCandidateShell, autoReleaseCandidatePackages } from '../candidates.js';
|
|
7
|
+
import type { ReleasePackageInfo } from '../core.js';
|
|
8
|
+
import { git, tag, withFixtureRepo, writePackage } from './helpers/fixture-repo.js';
|
|
9
|
+
|
|
10
|
+
const a: ReleasePackageInfo = { name: '@scope/a', projectName: 'a', path: 'packages/a', version: '1.0.0' };
|
|
11
|
+
const b: ReleasePackageInfo = { name: '@scope/b', projectName: 'b', path: 'packages/b', version: '1.0.0' };
|
|
12
|
+
const c: ReleasePackageInfo = { name: '@scope/c', projectName: 'c', path: 'packages/c', version: '0.1.0' };
|
|
13
|
+
const d: ReleasePackageInfo = { name: '@scope/d', projectName: 'd', path: 'packages/d', version: '0.1.0' };
|
|
14
|
+
const cli: ReleasePackageInfo = { name: '@scope/cli', projectName: 'cli', path: 'packages/cli', version: '0.2.0' };
|
|
15
|
+
const next: ReleasePackageInfo = {
|
|
16
|
+
name: '@scope/next',
|
|
17
|
+
projectName: 'next',
|
|
18
|
+
path: 'packages/next',
|
|
19
|
+
version: '1.1.0-next.0',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('auto release candidate filtering', () => {
|
|
23
|
+
it('selects tagged packages only when their package path changed', async () => {
|
|
24
|
+
await withFixtureRepo(async (root) => {
|
|
25
|
+
await writePackage(root, a.name, a.path, a.version);
|
|
26
|
+
await writePackage(root, b.name, b.path, b.version);
|
|
27
|
+
await git(root, ['add', '.']);
|
|
28
|
+
await git(root, ['commit', '-m', 'initial packages']);
|
|
29
|
+
await tag(root, 'a@1.0.0', '2025-01-01T00:00:00Z');
|
|
30
|
+
await tag(root, 'b@1.0.0', '2025-01-01T00:00:01Z');
|
|
31
|
+
|
|
32
|
+
await writeFile(join(root, 'README.md'), 'root-only change\n');
|
|
33
|
+
await git(root, ['add', 'README.md']);
|
|
34
|
+
await git(root, ['commit', '-m', 'docs: root only']);
|
|
35
|
+
|
|
36
|
+
await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [a, b])).resolves.toEqual([]);
|
|
37
|
+
|
|
38
|
+
await mkdir(join(root, a.path, 'src'), { recursive: true });
|
|
39
|
+
await writeFile(join(root, a.path, 'src/index.ts'), 'export const changed = true;\n');
|
|
40
|
+
await git(root, ['add', join(a.path, 'src/index.ts')]);
|
|
41
|
+
await git(root, ['commit', '-m', 'feat(a): package local']);
|
|
42
|
+
|
|
43
|
+
await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [a, b])).resolves.toEqual([a]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('ignores package-local config changes that do not affect published users', async () => {
|
|
48
|
+
await withFixtureRepo(async (root) => {
|
|
49
|
+
await writePackage(root, a.name, a.path, a.version);
|
|
50
|
+
await git(root, ['add', '.']);
|
|
51
|
+
await git(root, ['commit', '-m', 'initial package']);
|
|
52
|
+
await tag(root, 'a@1.0.0', '2025-01-01T00:00:00Z');
|
|
53
|
+
|
|
54
|
+
await mkdir(join(root, a.path), { recursive: true });
|
|
55
|
+
await writeFile(
|
|
56
|
+
join(root, a.path, 'package.json'),
|
|
57
|
+
`${JSON.stringify(
|
|
58
|
+
{
|
|
59
|
+
name: a.name,
|
|
60
|
+
version: a.version,
|
|
61
|
+
scripts: { test: 'nx run a:test' },
|
|
62
|
+
devDependencies: { typescript: '^5.9.3' },
|
|
63
|
+
nx: { name: 'a' },
|
|
64
|
+
},
|
|
65
|
+
null,
|
|
66
|
+
2,
|
|
67
|
+
)}\n`,
|
|
68
|
+
);
|
|
69
|
+
await writeFile(join(root, a.path, 'tsconfig.test.json'), `${JSON.stringify({ compilerOptions: {} })}\n`);
|
|
70
|
+
await git(root, ['add', '.']);
|
|
71
|
+
await git(root, ['commit', '-m', 'chore(a): normalize package config']);
|
|
72
|
+
|
|
73
|
+
await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [a])).resolves.toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('selects package manifest changes that affect published users', async () => {
|
|
78
|
+
await withFixtureRepo(async (root) => {
|
|
79
|
+
await writePackage(root, a.name, a.path, a.version);
|
|
80
|
+
await git(root, ['add', '.']);
|
|
81
|
+
await git(root, ['commit', '-m', 'initial package']);
|
|
82
|
+
await tag(root, 'a@1.0.0', '2025-01-01T00:00:00Z');
|
|
83
|
+
|
|
84
|
+
await writeFile(
|
|
85
|
+
join(root, a.path, 'package.json'),
|
|
86
|
+
`${JSON.stringify(
|
|
87
|
+
{
|
|
88
|
+
name: a.name,
|
|
89
|
+
version: a.version,
|
|
90
|
+
exports: { '.': './dist/index.js' },
|
|
91
|
+
dependencies: { '@scope/shared': '^1.0.0' },
|
|
92
|
+
},
|
|
93
|
+
null,
|
|
94
|
+
2,
|
|
95
|
+
)}\n`,
|
|
96
|
+
);
|
|
97
|
+
await git(root, ['add', join(a.path, 'package.json')]);
|
|
98
|
+
await git(root, ['commit', '-m', 'fix(a): update public entrypoint']);
|
|
99
|
+
|
|
100
|
+
await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [a])).resolves.toEqual([a]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('selects files included by package manifest files entries', async () => {
|
|
105
|
+
await withFixtureRepo(async (root) => {
|
|
106
|
+
await mkdir(join(root, a.path), { recursive: true });
|
|
107
|
+
await writeFile(
|
|
108
|
+
join(root, a.path, 'package.json'),
|
|
109
|
+
`${JSON.stringify({ name: a.name, version: a.version, files: ['schema'] }, null, 2)}\n`,
|
|
110
|
+
);
|
|
111
|
+
await mkdir(join(root, a.path, 'schema'), { recursive: true });
|
|
112
|
+
await writeFile(join(root, a.path, 'schema/index.json'), '{}\n');
|
|
113
|
+
await git(root, ['add', '.']);
|
|
114
|
+
await git(root, ['commit', '-m', 'initial package']);
|
|
115
|
+
await tag(root, 'a@1.0.0', '2025-01-01T00:00:00Z');
|
|
116
|
+
|
|
117
|
+
await writeFile(join(root, a.path, 'schema/index.json'), '{"changed":true}\n');
|
|
118
|
+
await git(root, ['add', join(a.path, 'schema/index.json')]);
|
|
119
|
+
await git(root, ['commit', '-m', 'fix(a): update shipped schema']);
|
|
120
|
+
|
|
121
|
+
await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [a])).resolves.toEqual([a]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('selects package changes from Nx build input patterns', async () => {
|
|
126
|
+
await withFixtureRepo(async (root) => {
|
|
127
|
+
await writePackage(root, a.name, a.path, a.version);
|
|
128
|
+
await git(root, ['add', '.']);
|
|
129
|
+
await git(root, ['commit', '-m', 'initial package']);
|
|
130
|
+
await tag(root, 'a@1.0.0', '2025-01-01T00:00:00Z');
|
|
131
|
+
|
|
132
|
+
await mkdir(join(root, a.path, 'generated'), { recursive: true });
|
|
133
|
+
await writeFile(join(root, a.path, 'generated/schema.ts'), 'export const schema = 1;\n');
|
|
134
|
+
await git(root, ['add', join(a.path, 'generated/schema.ts')]);
|
|
135
|
+
await git(root, ['commit', '-m', 'fix(a): update generated build input']);
|
|
136
|
+
|
|
137
|
+
await expect(
|
|
138
|
+
autoReleaseCandidatePackages(gitCandidateShell(root, { buildInputPatterns: ['generated/**/*'] }), [a]),
|
|
139
|
+
).resolves.toEqual([a]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('uses precise Nx production inputs to ignore config and test-only package paths', async () => {
|
|
144
|
+
await withFixtureRepo(async (root) => {
|
|
145
|
+
await writePackage(root, a.name, a.path, a.version);
|
|
146
|
+
await git(root, ['add', '.']);
|
|
147
|
+
await git(root, ['commit', '-m', 'initial package']);
|
|
148
|
+
await tag(root, 'a@1.0.0', '2025-01-01T00:00:00Z');
|
|
149
|
+
|
|
150
|
+
const shell = gitCandidateShell(root, { buildInputPatterns: ['src/**/*.ts', '!src/**/*.test.ts'] });
|
|
151
|
+
|
|
152
|
+
await mkdir(join(root, a.path, 'src'), { recursive: true });
|
|
153
|
+
await writeFile(join(root, a.path, 'vite.config.ts'), 'export default {};\n');
|
|
154
|
+
await writeFile(join(root, a.path, 'src/index.test.ts'), 'export const testOnly = true;\n');
|
|
155
|
+
await git(root, ['add', join(a.path, 'vite.config.ts'), join(a.path, 'src/index.test.ts')]);
|
|
156
|
+
await git(root, ['commit', '-m', 'test(a): update local-only inputs']);
|
|
157
|
+
|
|
158
|
+
await expect(autoReleaseCandidatePackages(shell, [a])).resolves.toEqual([]);
|
|
159
|
+
|
|
160
|
+
await writeFile(join(root, a.path, 'src/index.ts'), 'export const shipped = true;\n');
|
|
161
|
+
await git(root, ['add', join(a.path, 'src/index.ts')]);
|
|
162
|
+
await git(root, ['commit', '-m', 'fix(a): update production input']);
|
|
163
|
+
|
|
164
|
+
await expect(autoReleaseCandidatePackages(shell, [a])).resolves.toEqual([a]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('includes untagged packages only when their package path has history', async () => {
|
|
169
|
+
await withFixtureRepo(async (root) => {
|
|
170
|
+
await writePackage(root, c.name, c.path, c.version);
|
|
171
|
+
await git(root, ['add', '.']);
|
|
172
|
+
await git(root, ['commit', '-m', 'feat(c): add package']);
|
|
173
|
+
|
|
174
|
+
await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [c, d])).resolves.toEqual([c]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('does not auto-release an untagged next prerelease package version', async () => {
|
|
179
|
+
await withFixtureRepo(async (root) => {
|
|
180
|
+
await writePackage(root, next.name, next.path, next.version);
|
|
181
|
+
await git(root, ['add', '.']);
|
|
182
|
+
await git(root, ['commit', '-m', 'chore(release): prepare next prerelease']);
|
|
183
|
+
|
|
184
|
+
await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [next])).resolves.toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('uses a repaired project-name tag as the follow-up auto release baseline for a scoped package', async () => {
|
|
189
|
+
await withFixtureRepo(async (root) => {
|
|
190
|
+
await writePackage(root, cli.name, cli.path, cli.version);
|
|
191
|
+
await git(root, ['add', '.']);
|
|
192
|
+
await git(root, ['commit', '-m', 'chore(release): publish']);
|
|
193
|
+
await tag(root, 'cli@0.2.0', '2025-01-01T00:00:00Z');
|
|
194
|
+
|
|
195
|
+
await mkdir(join(root, cli.path, 'src'), { recursive: true });
|
|
196
|
+
await writeFile(join(root, cli.path, 'src/index.ts'), 'export const fixed = true;\n');
|
|
197
|
+
await git(root, ['add', join(cli.path, 'src/index.ts')]);
|
|
198
|
+
await git(root, ['commit', '-m', 'fix(cli): repair publish flow']);
|
|
199
|
+
|
|
200
|
+
const queriedRefs: string[] = [];
|
|
201
|
+
const candidates = await autoReleaseCandidatePackages(
|
|
202
|
+
gitCandidateShell(root, { buildInputPatterns: ['src/**/*.ts'], queriedRefs }),
|
|
203
|
+
[cli],
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
expect(candidates).toEqual([cli]);
|
|
207
|
+
expect(queriedRefs).toEqual(['refs/tags/cli@0.2.0']);
|
|
208
|
+
expect(queriedRefs).not.toContain('refs/tags/@scope/cli@0.2.0');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
function gitCandidateShell(
|
|
214
|
+
root: string,
|
|
215
|
+
options: { buildInputPatterns?: string[]; queriedRefs?: string[] } = {},
|
|
216
|
+
): AutoReleaseCandidateShell {
|
|
217
|
+
return {
|
|
218
|
+
gitRefExists: async (ref) => {
|
|
219
|
+
options.queriedRefs?.push(ref);
|
|
220
|
+
const result = await $`git rev-parse --verify ${ref}`.cwd(root).quiet().nothrow();
|
|
221
|
+
return result.exitCode === 0;
|
|
222
|
+
},
|
|
223
|
+
packageChangedFilesSince: async (ref, packagePath) => {
|
|
224
|
+
const result = await $`git diff --name-only ${`${ref}..HEAD`} -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
225
|
+
if (result.exitCode === 0) {
|
|
226
|
+
const packagePrefix = `${packagePath}/`;
|
|
227
|
+
return new TextDecoder()
|
|
228
|
+
.decode(result.stdout)
|
|
229
|
+
.split('\n')
|
|
230
|
+
.map((path) => path.trim())
|
|
231
|
+
.filter(Boolean)
|
|
232
|
+
.map((path) => (path.startsWith(packagePrefix) ? path.slice(packagePrefix.length) : path));
|
|
233
|
+
}
|
|
234
|
+
throw new Error(`Unable to inspect package changes under ${packagePath}.`);
|
|
235
|
+
},
|
|
236
|
+
packageJsonAtRef: async (ref, packagePath) => {
|
|
237
|
+
const result = await $`git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
|
|
238
|
+
if (result.exitCode !== 0) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
return parseJsonObject(new TextDecoder().decode(result.stdout));
|
|
242
|
+
},
|
|
243
|
+
currentPackageJson: async (packagePath) =>
|
|
244
|
+
parseJsonObject(await readFile(join(root, packagePath, 'package.json'), 'utf8')),
|
|
245
|
+
packageBuildInputPatterns: async () => options.buildInputPatterns ?? ['**/*'],
|
|
246
|
+
packageHasHistory: async (packagePath) => {
|
|
247
|
+
const result = await $`git log --format=%H -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
248
|
+
if (result.exitCode !== 0) {
|
|
249
|
+
throw new Error(`Unable to inspect package history under ${packagePath}.`);
|
|
250
|
+
}
|
|
251
|
+
return new TextDecoder().decode(result.stdout).trim().length > 0;
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseJsonObject(text: string): Record<string, unknown> {
|
|
257
|
+
const parsed = JSON.parse(text);
|
|
258
|
+
if (!isRecord(parsed)) {
|
|
259
|
+
throw new Error('expected JSON object');
|
|
260
|
+
}
|
|
261
|
+
return parsed;
|
|
262
|
+
}
|
|
@@ -4,9 +4,9 @@ import { groupReleaseTargets, pendingReleaseTargets, type ReleasePackageInfo, ty
|
|
|
4
4
|
import { planPublishActions } from '../publish-plan.js';
|
|
5
5
|
|
|
6
6
|
const packages: ReleasePackageInfo[] = [
|
|
7
|
-
{ name: '@scope/a', path: 'packages/a', version: '1.0.0' },
|
|
8
|
-
{ name: '@scope/b', path: 'packages/b', version: '2.0.0-beta.1' },
|
|
9
|
-
{ name: '@scope/c', path: 'packages/c', version: '3.0.0' },
|
|
7
|
+
{ name: '@scope/a', projectName: 'a', path: 'packages/a', version: '1.0.0' },
|
|
8
|
+
{ name: '@scope/b', projectName: 'b', path: 'packages/b', version: '2.0.0-beta.1' },
|
|
9
|
+
{ name: '@scope/c', projectName: 'c', path: 'packages/c', version: '3.0.0' },
|
|
10
10
|
];
|
|
11
11
|
|
|
12
12
|
describe('release core properties', () => {
|
|
@@ -77,7 +77,7 @@ describe('release core properties', () => {
|
|
|
77
77
|
fc.property(packageSubset(), packageSubset(), (npmMissingPackages, githubMissingPackages) => {
|
|
78
78
|
const plan = planPublishActions({ releasePackages: packages, npmMissingPackages, githubMissingPackages });
|
|
79
79
|
|
|
80
|
-
expect(plan.buildProjects).toEqual(npmMissingPackages.map((pkg) => pkg.
|
|
80
|
+
expect(plan.buildProjects).toEqual(npmMissingPackages.map((pkg) => pkg.projectName));
|
|
81
81
|
expect(plan.publishPackages.map((action) => action.pkg.name)).toEqual(
|
|
82
82
|
npmMissingPackages.map((pkg) => pkg.name),
|
|
83
83
|
);
|
|
@@ -122,7 +122,7 @@ function releaseRecords(): fc.Arbitrary<ReleaseTagRecord[]> {
|
|
|
122
122
|
}
|
|
123
123
|
seen.add(key);
|
|
124
124
|
records.push({
|
|
125
|
-
tag: `${pkg.
|
|
125
|
+
tag: `${pkg.projectName}@${pkg.version}`,
|
|
126
126
|
sha,
|
|
127
127
|
timestamp: row.timestamp,
|
|
128
128
|
pkg,
|
|
@@ -139,7 +139,7 @@ function record(pkg: ReleasePackageInfo | undefined, sha: string): ReleaseTagRec
|
|
|
139
139
|
throw new Error('Missing test package fixture.');
|
|
140
140
|
}
|
|
141
141
|
return {
|
|
142
|
-
tag: `${pkg.
|
|
142
|
+
tag: `${pkg.projectName}@${pkg.version}`,
|
|
143
143
|
sha,
|
|
144
144
|
timestamp: 1,
|
|
145
145
|
pkg,
|
|
@@ -10,9 +10,15 @@ import {
|
|
|
10
10
|
type ReleaseTagRecord,
|
|
11
11
|
} from '../core.js';
|
|
12
12
|
|
|
13
|
-
const stablePackage: ReleasePackageInfo = {
|
|
13
|
+
const stablePackage: ReleasePackageInfo = {
|
|
14
|
+
name: '@scope/stable',
|
|
15
|
+
projectName: 'stable',
|
|
16
|
+
path: 'packages/stable',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
};
|
|
14
19
|
const prereleasePackage: ReleasePackageInfo = {
|
|
15
20
|
name: '@scope/prerelease',
|
|
21
|
+
projectName: 'prerelease',
|
|
16
22
|
path: 'packages/prerelease',
|
|
17
23
|
version: '2.0.0-beta.1',
|
|
18
24
|
};
|
|
@@ -24,7 +30,7 @@ function record(
|
|
|
24
30
|
needs: { npm?: boolean; github?: boolean },
|
|
25
31
|
): ReleaseTagRecord {
|
|
26
32
|
return {
|
|
27
|
-
tag: `${pkg.
|
|
33
|
+
tag: `${pkg.projectName}@${pkg.version}`,
|
|
28
34
|
sha,
|
|
29
35
|
timestamp,
|
|
30
36
|
pkg,
|
|
@@ -93,9 +99,9 @@ describe('release core scenario coverage', () => {
|
|
|
93
99
|
]);
|
|
94
100
|
const shell: ReleasePlanningShell = {
|
|
95
101
|
listReleaseTagsByCreatorDate: async () => [
|
|
96
|
-
tag('
|
|
102
|
+
tag('stable@1.0.0', 'reachable', 30),
|
|
97
103
|
tag('@other/pkg@9.9.9', 'reachable', 20),
|
|
98
|
-
tag('
|
|
104
|
+
tag('stable@1.0.1', 'unreachable', 10),
|
|
99
105
|
],
|
|
100
106
|
isAncestor: async (ancestor) => ancestor === 'reachable',
|
|
101
107
|
packageVersionAtRef: async (packagePath, ref) => {
|
|
@@ -109,14 +115,14 @@ describe('release core scenario coverage', () => {
|
|
|
109
115
|
};
|
|
110
116
|
|
|
111
117
|
const records = await collectOwnedReleaseTagRecords(
|
|
112
|
-
[{ name: stablePackage.name, path: stablePackage.path }],
|
|
118
|
+
[{ name: stablePackage.name, projectName: stablePackage.projectName, path: stablePackage.path }],
|
|
113
119
|
'selected-ref',
|
|
114
120
|
shell,
|
|
115
121
|
);
|
|
116
122
|
|
|
117
|
-
expect(records.map((releaseRecord) => releaseRecord.tag)).toEqual(['
|
|
123
|
+
expect(records.map((releaseRecord) => releaseRecord.tag)).toEqual(['stable@1.0.0']);
|
|
118
124
|
expect(packageVersionRefs).toEqual(['packages/stable:reachable']);
|
|
119
|
-
expect(durableTags).toEqual(['
|
|
125
|
+
expect(durableTags).toEqual(['stable@1.0.0']);
|
|
120
126
|
});
|
|
121
127
|
|
|
122
128
|
it('stops querying older package tags after the newest tag for that package is fully durable', async () => {
|
|
@@ -128,9 +134,9 @@ describe('release core scenario coverage', () => {
|
|
|
128
134
|
]);
|
|
129
135
|
const shell: ReleasePlanningShell = {
|
|
130
136
|
listReleaseTagsByCreatorDate: async () => [
|
|
131
|
-
tag('
|
|
132
|
-
tag('
|
|
133
|
-
tag('
|
|
137
|
+
tag('stable@1.1.0', 'newer-stable', 30),
|
|
138
|
+
tag('prerelease@2.0.0-beta.1', 'older-prerelease', 20),
|
|
139
|
+
tag('stable@1.0.0', 'older-stable', 10),
|
|
134
140
|
],
|
|
135
141
|
isAncestor: async () => true,
|
|
136
142
|
packageVersionAtRef: async (packagePath, ref) => versions.get(`${packagePath}:${ref}`) ?? null,
|
|
@@ -142,32 +148,33 @@ describe('release core scenario coverage', () => {
|
|
|
142
148
|
|
|
143
149
|
const records = await collectOwnedReleaseTagRecords(
|
|
144
150
|
[
|
|
145
|
-
{ name: stablePackage.name, path: stablePackage.path },
|
|
146
|
-
{ name: prereleasePackage.name, path: prereleasePackage.path },
|
|
151
|
+
{ name: stablePackage.name, projectName: stablePackage.projectName, path: stablePackage.path },
|
|
152
|
+
{ name: prereleasePackage.name, projectName: prereleasePackage.projectName, path: prereleasePackage.path },
|
|
147
153
|
],
|
|
148
154
|
'selected-ref',
|
|
149
155
|
shell,
|
|
150
156
|
);
|
|
151
157
|
|
|
152
|
-
expect(records.map((releaseRecord) => releaseRecord.tag)).toEqual([
|
|
153
|
-
|
|
154
|
-
'@scope/prerelease@2.0.0-beta.1',
|
|
155
|
-
]);
|
|
156
|
-
expect(durableTags).toEqual(['@scope/stable@1.1.0', '@scope/prerelease@2.0.0-beta.1']);
|
|
158
|
+
expect(records.map((releaseRecord) => releaseRecord.tag)).toEqual(['stable@1.1.0', 'prerelease@2.0.0-beta.1']);
|
|
159
|
+
expect(durableTags).toEqual(['stable@1.1.0', 'prerelease@2.0.0-beta.1']);
|
|
157
160
|
});
|
|
158
161
|
|
|
159
162
|
it('throws when a release tag version does not match package.json at the peeled commit', async () => {
|
|
160
163
|
const shell: ReleasePlanningShell = {
|
|
161
|
-
listReleaseTagsByCreatorDate: async () => [tag('
|
|
164
|
+
listReleaseTagsByCreatorDate: async () => [tag('stable@1.0.0', 'mismatch', 10)],
|
|
162
165
|
isAncestor: async () => true,
|
|
163
166
|
packageVersionAtRef: async () => '1.0.1',
|
|
164
167
|
durableTagState: async () => ({ npmPublished: true, githubReleaseExists: true }),
|
|
165
168
|
};
|
|
166
169
|
|
|
167
170
|
await expect(
|
|
168
|
-
collectOwnedReleaseTagRecords(
|
|
171
|
+
collectOwnedReleaseTagRecords(
|
|
172
|
+
[{ name: stablePackage.name, projectName: stablePackage.projectName, path: stablePackage.path }],
|
|
173
|
+
'selected-ref',
|
|
174
|
+
shell,
|
|
175
|
+
),
|
|
169
176
|
).rejects.toThrow(
|
|
170
|
-
'Release tag
|
|
177
|
+
'Release tag stable@1.0.0 points at mismatch, but packages/stable/package.json has version 1.0.1.',
|
|
171
178
|
);
|
|
172
179
|
});
|
|
173
180
|
});
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
import {
|
|
3
|
+
collectOwnedReleaseTagRecords,
|
|
3
4
|
groupReleaseTargets,
|
|
5
|
+
legacyReleaseTag,
|
|
4
6
|
npmDistTagForVersion,
|
|
5
7
|
pendingReleaseTargets,
|
|
6
8
|
type ReleasePackageInfo,
|
|
7
9
|
type ReleaseTagRecord,
|
|
8
10
|
releasePackageForTag,
|
|
11
|
+
releaseTag,
|
|
12
|
+
releaseTagAliases,
|
|
9
13
|
} from '../core.js';
|
|
10
14
|
|
|
11
|
-
const a: ReleasePackageInfo = { name: '@scope/a', path: 'packages/a', version: '1.0.0' };
|
|
12
|
-
const b: ReleasePackageInfo = { name: '@scope/b', path: 'packages/b', version: '2.0.0-beta.1' };
|
|
15
|
+
const a: ReleasePackageInfo = { name: '@scope/a', projectName: 'a', path: 'packages/a', version: '1.0.0' };
|
|
16
|
+
const b: ReleasePackageInfo = { name: '@scope/b', projectName: 'b', path: 'packages/b', version: '2.0.0-beta.1' };
|
|
13
17
|
|
|
14
18
|
function record(
|
|
15
19
|
pkg: ReleasePackageInfo,
|
|
@@ -18,7 +22,7 @@ function record(
|
|
|
18
22
|
needs: { npm?: boolean; github?: boolean },
|
|
19
23
|
): ReleaseTagRecord {
|
|
20
24
|
return {
|
|
21
|
-
tag: `${pkg.
|
|
25
|
+
tag: `${pkg.projectName}@${pkg.version}`,
|
|
22
26
|
sha,
|
|
23
27
|
timestamp,
|
|
24
28
|
pkg,
|
|
@@ -29,8 +33,9 @@ function record(
|
|
|
29
33
|
|
|
30
34
|
describe('release core planning', () => {
|
|
31
35
|
it('matches owned release tags without semantic version parsing', () => {
|
|
32
|
-
expect(releasePackageForTag([a], '
|
|
33
|
-
expect(releasePackageForTag([a], '@scope/
|
|
36
|
+
expect(releasePackageForTag([a], 'a@1.2.3')?.version).toBe('1.2.3');
|
|
37
|
+
expect(releasePackageForTag([a], '@scope/a@1.2.3')).toBeNull();
|
|
38
|
+
expect(releasePackageForTag([a], 'c@1.2.3')).toBeNull();
|
|
34
39
|
});
|
|
35
40
|
|
|
36
41
|
it('groups only repair-needed tags by commit', () => {
|
|
@@ -70,4 +75,46 @@ describe('release core planning', () => {
|
|
|
70
75
|
expect(npmDistTagForVersion('1.0.0')).toBe('latest');
|
|
71
76
|
expect(npmDistTagForVersion('1.0.0-beta.1')).toBe('next');
|
|
72
77
|
});
|
|
78
|
+
|
|
79
|
+
it('keeps project-name release tags canonical while recognizing legacy package-name aliases', () => {
|
|
80
|
+
const scoped = { name: '@scope/a', projectName: 'a', version: '1.0.0' };
|
|
81
|
+
|
|
82
|
+
expect(releaseTag(scoped)).toBe('a@1.0.0');
|
|
83
|
+
expect(legacyReleaseTag(scoped)).toBe('@scope/a@1.0.0');
|
|
84
|
+
expect(releaseTagAliases(scoped)).toEqual(['a@1.0.0', '@scope/a@1.0.0']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('checks durable release state concurrently while preserving tag-order records', async () => {
|
|
88
|
+
const durableCalls: string[] = [];
|
|
89
|
+
let active = 0;
|
|
90
|
+
let maxActive = 0;
|
|
91
|
+
let unblockDurableCalls: (() => void) | undefined;
|
|
92
|
+
const durableCallsBlocked = new Promise<void>((resolve) => {
|
|
93
|
+
unblockDurableCalls = resolve;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const records = await collectOwnedReleaseTagRecords([a, b], 'head', {
|
|
97
|
+
listReleaseTagsByCreatorDate: async () => [
|
|
98
|
+
{ name: 'a@1.0.0', sha: 'newer', timestamp: 2 },
|
|
99
|
+
{ name: 'b@2.0.0-beta.1', sha: 'older', timestamp: 1 },
|
|
100
|
+
],
|
|
101
|
+
isAncestor: async () => true,
|
|
102
|
+
packageVersionAtRef: async (packagePath) => (packagePath === a.path ? a.version : b.version),
|
|
103
|
+
durableTagState: async (_pkg, tag) => {
|
|
104
|
+
durableCalls.push(tag);
|
|
105
|
+
active += 1;
|
|
106
|
+
maxActive = Math.max(maxActive, active);
|
|
107
|
+
if (durableCalls.length === 2) {
|
|
108
|
+
unblockDurableCalls?.();
|
|
109
|
+
}
|
|
110
|
+
await durableCallsBlocked;
|
|
111
|
+
active -= 1;
|
|
112
|
+
return { npmPublished: false, githubReleaseExists: false };
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(durableCalls).toEqual(['a@1.0.0', 'b@2.0.0-beta.1']);
|
|
117
|
+
expect(maxActive).toBeGreaterThan(1);
|
|
118
|
+
expect(records.map((releaseRecord) => releaseRecord.tag)).toEqual(['a@1.0.0', 'b@2.0.0-beta.1']);
|
|
119
|
+
});
|
|
73
120
|
});
|