@smoothbricks/cli 0.1.1 → 0.2.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.
Files changed (117) hide show
  1. package/README.md +155 -30
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +91 -7
  4. package/dist/devenv/index.d.ts +10 -0
  5. package/dist/devenv/index.d.ts.map +1 -0
  6. package/dist/devenv/index.js +27 -0
  7. package/dist/lib/cli-package.d.ts +4 -0
  8. package/dist/lib/cli-package.d.ts.map +1 -0
  9. package/dist/lib/cli-package.js +15 -0
  10. package/dist/lib/run.d.ts +5 -0
  11. package/dist/lib/run.d.ts.map +1 -1
  12. package/dist/lib/run.js +13 -0
  13. package/dist/lib/workspace.d.ts +15 -1
  14. package/dist/lib/workspace.d.ts.map +1 -1
  15. package/dist/lib/workspace.js +25 -2
  16. package/dist/monorepo/commit-msg.d.ts +8 -1
  17. package/dist/monorepo/commit-msg.d.ts.map +1 -1
  18. package/dist/monorepo/commit-msg.js +85 -7
  19. package/dist/monorepo/index.d.ts +19 -2
  20. package/dist/monorepo/index.d.ts.map +1 -1
  21. package/dist/monorepo/index.js +104 -16
  22. package/dist/monorepo/managed-files.js +1 -1
  23. package/dist/monorepo/nx-sync.d.ts +2 -1
  24. package/dist/monorepo/nx-sync.d.ts.map +1 -1
  25. package/dist/monorepo/nx-sync.js +8 -2
  26. package/dist/monorepo/package-hygiene.d.ts +6 -2
  27. package/dist/monorepo/package-hygiene.d.ts.map +1 -1
  28. package/dist/monorepo/package-hygiene.js +12 -4
  29. package/dist/monorepo/package-policy.d.ts +22 -2
  30. package/dist/monorepo/package-policy.d.ts.map +1 -1
  31. package/dist/monorepo/package-policy.js +1167 -8
  32. package/dist/monorepo/packed-package.d.ts +3 -0
  33. package/dist/monorepo/packed-package.d.ts.map +1 -1
  34. package/dist/monorepo/packed-package.js +68 -36
  35. package/dist/monorepo/packs/index.d.ts +20 -1
  36. package/dist/monorepo/packs/index.d.ts.map +1 -1
  37. package/dist/monorepo/packs/index.js +238 -19
  38. package/dist/monorepo/publish-workflow.d.ts +0 -4
  39. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  40. package/dist/monorepo/publish-workflow.js +5 -12
  41. package/dist/monorepo/tool-validation.d.ts +22 -0
  42. package/dist/monorepo/tool-validation.d.ts.map +1 -0
  43. package/dist/monorepo/tool-validation.js +399 -0
  44. package/dist/nx/index.d.ts +24 -0
  45. package/dist/nx/index.d.ts.map +1 -0
  46. package/dist/nx/index.js +94 -0
  47. package/dist/release/bootstrap-npm-packages.d.ts +19 -0
  48. package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
  49. package/dist/release/bootstrap-npm-packages.js +58 -0
  50. package/dist/release/candidates.d.ts +11 -0
  51. package/dist/release/candidates.d.ts.map +1 -0
  52. package/dist/release/candidates.js +181 -0
  53. package/dist/release/core.d.ts +1 -0
  54. package/dist/release/core.d.ts.map +1 -1
  55. package/dist/release/github-release.js +2 -2
  56. package/dist/release/index.d.ts +25 -0
  57. package/dist/release/index.d.ts.map +1 -1
  58. package/dist/release/index.js +247 -49
  59. package/dist/release/npm-auth.d.ts +0 -1
  60. package/dist/release/npm-auth.d.ts.map +1 -1
  61. package/dist/release/npm-auth.js +4 -16
  62. package/dist/release/orchestration.d.ts +2 -1
  63. package/dist/release/orchestration.d.ts.map +1 -1
  64. package/dist/release/orchestration.js +9 -1
  65. package/dist/release/publish-plan.js +1 -1
  66. package/managed/raw/git-format-staged.yml +54 -0
  67. package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
  68. package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
  69. package/managed/templates/github/workflows/ci.yml +1 -0
  70. package/package.json +15 -3
  71. package/src/cli.ts +134 -20
  72. package/src/devenv/index.test.ts +49 -0
  73. package/src/devenv/index.ts +38 -0
  74. package/src/lib/cli-package.ts +18 -0
  75. package/src/lib/run.ts +19 -0
  76. package/src/lib/workspace.ts +41 -2
  77. package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
  78. package/src/monorepo/commit-msg.test.ts +45 -0
  79. package/src/monorepo/commit-msg.ts +107 -7
  80. package/src/monorepo/index.ts +142 -16
  81. package/src/monorepo/managed-files.ts +1 -1
  82. package/src/monorepo/nx-sync.ts +9 -2
  83. package/src/monorepo/package-hygiene.test.ts +34 -0
  84. package/src/monorepo/package-hygiene.ts +26 -4
  85. package/src/monorepo/package-policy.test.ts +902 -0
  86. package/src/monorepo/package-policy.ts +1393 -7
  87. package/src/monorepo/packed-package.ts +89 -37
  88. package/src/monorepo/packs/index.test.ts +189 -0
  89. package/src/monorepo/packs/index.ts +311 -20
  90. package/src/monorepo/publish-workflow.ts +8 -16
  91. package/src/monorepo/setup-test-tracing.test.ts +120 -0
  92. package/src/monorepo/tool-validation.test.ts +261 -0
  93. package/src/monorepo/tool-validation.ts +462 -0
  94. package/src/nx/index.test.ts +64 -0
  95. package/src/nx/index.ts +119 -0
  96. package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
  97. package/src/release/__tests__/candidates.test.ts +217 -0
  98. package/src/release/__tests__/core-properties.test.ts +4 -4
  99. package/src/release/__tests__/core-scenarios.test.ts +15 -5
  100. package/src/release/__tests__/core.test.ts +2 -2
  101. package/src/release/__tests__/fixture-repo.test.ts +22 -13
  102. package/src/release/__tests__/github-release.test.ts +13 -8
  103. package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
  104. package/src/release/__tests__/npm-auth.test.ts +12 -21
  105. package/src/release/__tests__/orchestration.test.ts +51 -7
  106. package/src/release/__tests__/publish-plan.test.ts +9 -3
  107. package/src/release/__tests__/retag-unpublished.test.ts +1 -1
  108. package/src/release/__tests__/trust-publisher.test.ts +133 -0
  109. package/src/release/bootstrap-npm-packages.ts +88 -0
  110. package/src/release/candidates.ts +225 -0
  111. package/src/release/core.ts +1 -0
  112. package/src/release/github-release.ts +2 -2
  113. package/src/release/index.ts +346 -56
  114. package/src/release/npm-auth.ts +4 -33
  115. package/src/release/orchestration.ts +12 -2
  116. package/src/release/publish-plan.ts +1 -1
  117. package/managed/raw/.git-format-staged.yml +0 -47
@@ -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,217 @@
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
+
15
+ describe('auto release candidate filtering', () => {
16
+ it('selects tagged packages only when their package path changed', async () => {
17
+ await withFixtureRepo(async (root) => {
18
+ await writePackage(root, a.name, a.path, a.version);
19
+ await writePackage(root, b.name, b.path, b.version);
20
+ await git(root, ['add', '.']);
21
+ await git(root, ['commit', '-m', 'initial packages']);
22
+ await tag(root, '@scope/a@1.0.0', '2025-01-01T00:00:00Z');
23
+ await tag(root, '@scope/b@1.0.0', '2025-01-01T00:00:01Z');
24
+
25
+ await writeFile(join(root, 'README.md'), 'root-only change\n');
26
+ await git(root, ['add', 'README.md']);
27
+ await git(root, ['commit', '-m', 'docs: root only']);
28
+
29
+ await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [a, b])).resolves.toEqual([]);
30
+
31
+ await mkdir(join(root, a.path, 'src'), { recursive: true });
32
+ await writeFile(join(root, a.path, 'src/index.ts'), 'export const changed = true;\n');
33
+ await git(root, ['add', join(a.path, 'src/index.ts')]);
34
+ await git(root, ['commit', '-m', 'feat(a): package local']);
35
+
36
+ await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [a, b])).resolves.toEqual([a]);
37
+ });
38
+ });
39
+
40
+ it('ignores package-local config changes that do not affect published users', async () => {
41
+ await withFixtureRepo(async (root) => {
42
+ await writePackage(root, a.name, a.path, a.version);
43
+ await git(root, ['add', '.']);
44
+ await git(root, ['commit', '-m', 'initial package']);
45
+ await tag(root, '@scope/a@1.0.0', '2025-01-01T00:00:00Z');
46
+
47
+ await mkdir(join(root, a.path), { recursive: true });
48
+ await writeFile(
49
+ join(root, a.path, 'package.json'),
50
+ `${JSON.stringify(
51
+ {
52
+ name: a.name,
53
+ version: a.version,
54
+ scripts: { test: 'nx run a:test' },
55
+ devDependencies: { typescript: '^5.9.3' },
56
+ nx: { name: 'a' },
57
+ },
58
+ null,
59
+ 2,
60
+ )}\n`,
61
+ );
62
+ await writeFile(join(root, a.path, 'tsconfig.test.json'), `${JSON.stringify({ compilerOptions: {} })}\n`);
63
+ await git(root, ['add', '.']);
64
+ await git(root, ['commit', '-m', 'chore(a): normalize package config']);
65
+
66
+ await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [a])).resolves.toEqual([]);
67
+ });
68
+ });
69
+
70
+ it('selects package manifest changes that affect published users', async () => {
71
+ await withFixtureRepo(async (root) => {
72
+ await writePackage(root, a.name, a.path, a.version);
73
+ await git(root, ['add', '.']);
74
+ await git(root, ['commit', '-m', 'initial package']);
75
+ await tag(root, '@scope/a@1.0.0', '2025-01-01T00:00:00Z');
76
+
77
+ await writeFile(
78
+ join(root, a.path, 'package.json'),
79
+ `${JSON.stringify(
80
+ {
81
+ name: a.name,
82
+ version: a.version,
83
+ exports: { '.': './dist/index.js' },
84
+ dependencies: { '@scope/shared': '^1.0.0' },
85
+ },
86
+ null,
87
+ 2,
88
+ )}\n`,
89
+ );
90
+ await git(root, ['add', join(a.path, 'package.json')]);
91
+ await git(root, ['commit', '-m', 'fix(a): update public entrypoint']);
92
+
93
+ await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [a])).resolves.toEqual([a]);
94
+ });
95
+ });
96
+
97
+ it('selects files included by package manifest files entries', async () => {
98
+ await withFixtureRepo(async (root) => {
99
+ await mkdir(join(root, a.path), { recursive: true });
100
+ await writeFile(
101
+ join(root, a.path, 'package.json'),
102
+ `${JSON.stringify({ name: a.name, version: a.version, files: ['schema'] }, null, 2)}\n`,
103
+ );
104
+ await mkdir(join(root, a.path, 'schema'), { recursive: true });
105
+ await writeFile(join(root, a.path, 'schema/index.json'), '{}\n');
106
+ await git(root, ['add', '.']);
107
+ await git(root, ['commit', '-m', 'initial package']);
108
+ await tag(root, '@scope/a@1.0.0', '2025-01-01T00:00:00Z');
109
+
110
+ await writeFile(join(root, a.path, 'schema/index.json'), '{"changed":true}\n');
111
+ await git(root, ['add', join(a.path, 'schema/index.json')]);
112
+ await git(root, ['commit', '-m', 'fix(a): update shipped schema']);
113
+
114
+ await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [a])).resolves.toEqual([a]);
115
+ });
116
+ });
117
+
118
+ it('selects package changes from Nx build input patterns', async () => {
119
+ await withFixtureRepo(async (root) => {
120
+ await writePackage(root, a.name, a.path, a.version);
121
+ await git(root, ['add', '.']);
122
+ await git(root, ['commit', '-m', 'initial package']);
123
+ await tag(root, '@scope/a@1.0.0', '2025-01-01T00:00:00Z');
124
+
125
+ await mkdir(join(root, a.path, 'generated'), { recursive: true });
126
+ await writeFile(join(root, a.path, 'generated/schema.ts'), 'export const schema = 1;\n');
127
+ await git(root, ['add', join(a.path, 'generated/schema.ts')]);
128
+ await git(root, ['commit', '-m', 'fix(a): update generated build input']);
129
+
130
+ await expect(
131
+ autoReleaseCandidatePackages(gitCandidateShell(root, { buildInputPatterns: ['generated/**/*'] }), [a]),
132
+ ).resolves.toEqual([a]);
133
+ });
134
+ });
135
+
136
+ it('uses precise Nx production inputs to ignore config and test-only package paths', async () => {
137
+ await withFixtureRepo(async (root) => {
138
+ await writePackage(root, a.name, a.path, a.version);
139
+ await git(root, ['add', '.']);
140
+ await git(root, ['commit', '-m', 'initial package']);
141
+ await tag(root, '@scope/a@1.0.0', '2025-01-01T00:00:00Z');
142
+
143
+ const shell = gitCandidateShell(root, { buildInputPatterns: ['src/**/*.ts', '!src/**/*.test.ts'] });
144
+
145
+ await mkdir(join(root, a.path, 'src'), { recursive: true });
146
+ await writeFile(join(root, a.path, 'vite.config.ts'), 'export default {};\n');
147
+ await writeFile(join(root, a.path, 'src/index.test.ts'), 'export const testOnly = true;\n');
148
+ await git(root, ['add', join(a.path, 'vite.config.ts'), join(a.path, 'src/index.test.ts')]);
149
+ await git(root, ['commit', '-m', 'test(a): update local-only inputs']);
150
+
151
+ await expect(autoReleaseCandidatePackages(shell, [a])).resolves.toEqual([]);
152
+
153
+ await writeFile(join(root, a.path, 'src/index.ts'), 'export const shipped = true;\n');
154
+ await git(root, ['add', join(a.path, 'src/index.ts')]);
155
+ await git(root, ['commit', '-m', 'fix(a): update production input']);
156
+
157
+ await expect(autoReleaseCandidatePackages(shell, [a])).resolves.toEqual([a]);
158
+ });
159
+ });
160
+
161
+ it('includes untagged packages only when their package path has history', async () => {
162
+ await withFixtureRepo(async (root) => {
163
+ await writePackage(root, c.name, c.path, c.version);
164
+ await git(root, ['add', '.']);
165
+ await git(root, ['commit', '-m', 'feat(c): add package']);
166
+
167
+ await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [c, d])).resolves.toEqual([c]);
168
+ });
169
+ });
170
+ });
171
+
172
+ function gitCandidateShell(root: string, options: { buildInputPatterns?: string[] } = {}): AutoReleaseCandidateShell {
173
+ return {
174
+ gitRefExists: async (ref) => {
175
+ const result = await $`git rev-parse --verify ${ref}`.cwd(root).quiet().nothrow();
176
+ return result.exitCode === 0;
177
+ },
178
+ packageChangedFilesSince: async (ref, packagePath) => {
179
+ const result = await $`git diff --name-only ${`${ref}..HEAD`} -- ${packagePath}`.cwd(root).quiet().nothrow();
180
+ if (result.exitCode === 0) {
181
+ const packagePrefix = `${packagePath}/`;
182
+ return new TextDecoder()
183
+ .decode(result.stdout)
184
+ .split('\n')
185
+ .map((path) => path.trim())
186
+ .filter(Boolean)
187
+ .map((path) => (path.startsWith(packagePrefix) ? path.slice(packagePrefix.length) : path));
188
+ }
189
+ throw new Error(`Unable to inspect package changes under ${packagePath}.`);
190
+ },
191
+ packageJsonAtRef: async (ref, packagePath) => {
192
+ const result = await $`git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
193
+ if (result.exitCode !== 0) {
194
+ return null;
195
+ }
196
+ return parseJsonObject(new TextDecoder().decode(result.stdout));
197
+ },
198
+ currentPackageJson: async (packagePath) =>
199
+ parseJsonObject(await readFile(join(root, packagePath, 'package.json'), 'utf8')),
200
+ packageBuildInputPatterns: async () => options.buildInputPatterns ?? ['**/*'],
201
+ packageHasHistory: async (packagePath) => {
202
+ const result = await $`git log --format=%H -- ${packagePath}`.cwd(root).quiet().nothrow();
203
+ if (result.exitCode !== 0) {
204
+ throw new Error(`Unable to inspect package history under ${packagePath}.`);
205
+ }
206
+ return new TextDecoder().decode(result.stdout).trim().length > 0;
207
+ },
208
+ };
209
+ }
210
+
211
+ function parseJsonObject(text: string): Record<string, unknown> {
212
+ const parsed = JSON.parse(text);
213
+ if (!isRecord(parsed)) {
214
+ throw new Error('expected JSON object');
215
+ }
216
+ return parsed;
217
+ }
@@ -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.name));
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
  );
@@ -10,9 +10,15 @@ import {
10
10
  type ReleaseTagRecord,
11
11
  } from '../core.js';
12
12
 
13
- const stablePackage: ReleasePackageInfo = { name: '@scope/stable', path: 'packages/stable', version: '1.0.0' };
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
  };
@@ -109,7 +115,7 @@ 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
  );
@@ -142,8 +148,8 @@ 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,
@@ -165,7 +171,11 @@ describe('release core scenario coverage', () => {
165
171
  };
166
172
 
167
173
  await expect(
168
- collectOwnedReleaseTagRecords([{ name: stablePackage.name, path: stablePackage.path }], 'selected-ref', shell),
174
+ collectOwnedReleaseTagRecords(
175
+ [{ name: stablePackage.name, projectName: stablePackage.projectName, path: stablePackage.path }],
176
+ 'selected-ref',
177
+ shell,
178
+ ),
169
179
  ).rejects.toThrow(
170
180
  'Release tag @scope/stable@1.0.0 points at mismatch, but packages/stable/package.json has version 1.0.1.',
171
181
  );
@@ -8,8 +8,8 @@ import {
8
8
  releasePackageForTag,
9
9
  } from '../core.js';
10
10
 
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' };
11
+ const a: ReleasePackageInfo = { name: '@scope/a', projectName: 'a', path: 'packages/a', version: '1.0.0' };
12
+ const b: ReleasePackageInfo = { name: '@scope/b', projectName: 'b', path: 'packages/b', version: '2.0.0-beta.1' };
13
13
 
14
14
  function record(
15
15
  pkg: ReleasePackageInfo,
@@ -44,8 +44,8 @@ describe('release planning with fixture git repositories', () => {
44
44
  const githubReleases = new Set(['@scope/a@1.0.0']);
45
45
  const records = await collectOwnedReleaseTagRecords(
46
46
  [
47
- { name: '@scope/a', path: 'packages/a' },
48
- { name: '@scope/b', path: 'packages/b' },
47
+ { name: '@scope/a', projectName: 'a', path: 'packages/a' },
48
+ { name: '@scope/b', projectName: 'b', path: 'packages/b' },
49
49
  ],
50
50
  head,
51
51
  {
@@ -83,12 +83,16 @@ describe('release planning with fixture git repositories', () => {
83
83
  await git(checkout, ['fetch', '--tags', 'origin', 'main']);
84
84
  const head = await gitOutput(checkout, ['rev-parse', 'HEAD']);
85
85
 
86
- const records = await collectOwnedReleaseTagRecords([{ name: '@scope/a', path: 'packages/a' }], head, {
87
- listReleaseTagsByCreatorDate: () => gitReleaseTagsByCreatorDate(checkout),
88
- isAncestor: (ancestor, descendant) => gitIsAncestor(checkout, ancestor, descendant),
89
- packageVersionAtRef: (packagePath, ref) => packageVersionAtRef(checkout, packagePath, ref),
90
- durableTagState: async () => ({ npmPublished: false, githubReleaseExists: false }),
91
- });
86
+ const records = await collectOwnedReleaseTagRecords(
87
+ [{ name: '@scope/a', projectName: 'a', path: 'packages/a' }],
88
+ head,
89
+ {
90
+ listReleaseTagsByCreatorDate: () => gitReleaseTagsByCreatorDate(checkout),
91
+ isAncestor: (ancestor, descendant) => gitIsAncestor(checkout, ancestor, descendant),
92
+ packageVersionAtRef: (packagePath, ref) => packageVersionAtRef(checkout, packagePath, ref),
93
+ durableTagState: async () => ({ npmPublished: false, githubReleaseExists: false }),
94
+ },
95
+ );
92
96
 
93
97
  expect(records.map((record) => record.tag)).toEqual(['@scope/a@1.0.0']);
94
98
  expect(pendingReleaseTargets(records, 'not-head').map((target) => target.sha)).toEqual([head]);
@@ -101,7 +105,7 @@ describe('release planning with fixture git repositories', () => {
101
105
  await writeBuildablePackage(root, '@scope/a', 'packages/a');
102
106
  await writeBuildablePackage(root, '@scope/b', 'packages/b');
103
107
 
104
- await $`nx run-many -t build --projects=${'@scope/a,@scope/b'}`.cwd(root).quiet();
108
+ await $`nx run-many -t build --projects=${'a,b'}`.cwd(root).quiet();
105
109
 
106
110
  await expect(readFile(join(root, 'packages/a/dist/index.js'), 'utf8')).resolves.toBe('{}\n');
107
111
  await expect(readFile(join(root, 'packages/b/dist/index.js'), 'utf8')).resolves.toBe('{}\n');
@@ -197,7 +201,12 @@ describe('release planning with fixture git repositories', () => {
197
201
  const runner = join(author, 'runner');
198
202
  await git(runner, ['config', 'user.name', 'Test User']);
199
203
  await git(runner, ['config', 'user.email', 'test@example.com']);
200
- const pkg: ReleasePackageInfo = { name: '@scope/pushed', path: 'packages/pushed', version: '1.0.0' };
204
+ const pkg: ReleasePackageInfo = {
205
+ name: '@scope/pushed',
206
+ projectName: 'pushed',
207
+ path: 'packages/pushed',
208
+ version: '1.0.0',
209
+ };
201
210
  const shell = new LocalGitRepairShell(runner);
202
211
 
203
212
  const summary = await completeReleaseAtHead(shell, [pkg], false, false);
@@ -216,8 +225,8 @@ describe('release planning with fixture git repositories', () => {
216
225
 
217
226
  function releaseFixturePackages(): ReleasePackageInfo[] {
218
227
  return [
219
- { name: '@scope/a', path: 'packages/a', version: '0.0.0' },
220
- { name: '@scope/b', path: 'packages/b', version: '0.0.0' },
228
+ { name: '@scope/a', projectName: 'a', path: 'packages/a', version: '0.0.0' },
229
+ { name: '@scope/b', projectName: 'b', path: 'packages/b', version: '0.0.0' },
221
230
  ];
222
231
  }
223
232
 
@@ -265,7 +274,7 @@ class LocalGitRepairShell implements ReleaseRepairShell<ReleasePackageInfo> {
265
274
 
266
275
  async buildReleaseCandidate(packages: ReleasePackageInfo[]): Promise<void> {
267
276
  this.builds.push(packages.map((pkg) => pkg.name));
268
- await $`nx run-many -t build --projects=${packages.map((pkg) => pkg.name).join(',')}`.cwd(this.root).quiet();
277
+ await $`nx run-many -t build --projects=${packages.map((pkg) => pkg.projectName).join(',')}`.cwd(this.root).quiet();
269
278
  }
270
279
 
271
280
  async publishPackage(pkg: ReleasePackageInfo, distTag: string, dryRun: boolean): Promise<void> {
@@ -8,14 +8,19 @@ import {
8
8
  projectChangelogContents,
9
9
  } from '../github-release.js';
10
10
 
11
- const stable: ReleasePackageInfo = { name: '@scope/pkg', path: 'packages/pkg', version: '1.2.3' };
12
- const prerelease: ReleasePackageInfo = { name: '@scope/pkg', path: 'packages/pkg', version: '2.0.0-beta.1' };
11
+ const stable: ReleasePackageInfo = { name: '@scope/pkg', projectName: 'pkg', path: 'packages/pkg', version: '1.2.3' };
12
+ const prerelease: ReleasePackageInfo = {
13
+ name: '@scope/pkg',
14
+ projectName: 'pkg',
15
+ path: 'packages/pkg',
16
+ version: '2.0.0-beta.1',
17
+ };
13
18
 
14
19
  describe('GitHub release helpers', () => {
15
20
  it('configures the Nx changelog API for render-only project changelogs', () => {
16
21
  expect(nxProjectChangelogArgs(stable, '@scope/pkg@1.2.2', false)).toEqual({
17
22
  version: '1.2.3',
18
- projects: ['@scope/pkg'],
23
+ projects: ['pkg'],
19
24
  gitCommit: false,
20
25
  gitTag: false,
21
26
  gitPush: false,
@@ -30,7 +35,7 @@ describe('GitHub release helpers', () => {
30
35
  it('marks the Nx changelog request as a first release when there is no previous tag', () => {
31
36
  expect(nxProjectChangelogArgs(stable, null, true)).toEqual({
32
37
  version: '1.2.3',
33
- projects: ['@scope/pkg'],
38
+ projects: ['pkg'],
34
39
  gitCommit: false,
35
40
  gitTag: false,
36
41
  gitPush: false,
@@ -47,17 +52,17 @@ describe('GitHub release helpers', () => {
47
52
  projectChangelogContents(
48
53
  {
49
54
  projectChangelogs: {
50
- '@scope/pkg': { contents: 'generated release notes' },
55
+ pkg: { contents: 'generated release notes' },
51
56
  },
52
57
  },
53
- '@scope/pkg',
58
+ 'pkg',
54
59
  ),
55
60
  ).toBe('generated release notes');
56
61
  });
57
62
 
58
63
  it('fails when Nx omits the requested project changelog', () => {
59
- expect(() => projectChangelogContents({ projectChangelogs: {} }, '@scope/pkg')).toThrow(
60
- 'Nx did not generate a project changelog for @scope/pkg.',
64
+ expect(() => projectChangelogContents({ projectChangelogs: {} }, 'pkg')).toThrow(
65
+ 'Nx did not generate a project changelog for pkg.',
61
66
  );
62
67
  });
63
68
 
@@ -47,6 +47,7 @@ export async function writeBuildablePackage(
47
47
  name,
48
48
  version,
49
49
  nx: {
50
+ name: name.startsWith('@') ? name.slice(name.indexOf('/') + 1) : name,
50
51
  targets: {
51
52
  build: {
52
53
  executor: 'nx:run-commands',
@@ -12,12 +12,10 @@ const pkg: Pick<ReleasePackageInfo, 'name' | 'version'> = { name: '@scope/pkg',
12
12
  describe('npm publish auth diagnostics', () => {
13
13
  it('explains trusted publishing setup when an existing package publish is unauthenticated', () => {
14
14
  const message = npmPublishAuthFailureMessage(pkg, {
15
- useBootstrapToken: false,
16
15
  tokenPresent: true,
17
16
  repository: 'smoothbricks/codebase',
18
17
  });
19
18
  const markdown = npmPublishAuthFailureMarkdown(pkg, {
20
- useBootstrapToken: false,
21
19
  tokenPresent: true,
22
20
  repository: 'smoothbricks/codebase',
23
21
  });
@@ -32,16 +30,12 @@ describe('npm publish auth diagnostics', () => {
32
30
  expect(markdown).toContain('repository `smoothbricks/codebase` and workflow `publish.yml`');
33
31
  });
34
32
 
35
- it('explains the bootstrap token path for first package publishes', () => {
36
- const message = npmPublishAuthFailureMessage(pkg, { useBootstrapToken: true, tokenPresent: false });
37
- const markdown = npmPublishAuthFailureMarkdown(pkg, { useBootstrapToken: true, tokenPresent: false });
33
+ it('points first package publishes to local placeholder bootstrap', () => {
34
+ const message = npmPublishAuthFailureMessage(pkg, { tokenPresent: false });
35
+ const markdown = npmPublishAuthFailureMarkdown(pkg, { tokenPresent: false });
38
36
 
39
- expect(message).toContain('temporary npm automation token');
40
- expect(message).toContain('NODE_AUTH_TOKEN/NPM_TOKEN is not set');
41
- expect(message).toContain('After the first successful publish, run:');
42
- expect(message).toContain('smoo release trust-publisher');
43
- expect(markdown).toContain('Add a temporary `NPM_TOKEN` repository secret');
44
- expect(markdown).toContain('After the first successful publish, run `smoo release trust-publisher`');
37
+ expect(message).toContain('smoo release trust-publisher --bootstrap');
38
+ expect(markdown).toContain('smoo release trust-publisher --bootstrap');
45
39
  });
46
40
 
47
41
  it('reports trusted-publishing guidance and writes it to the publish summary after existing package auth failure', async () => {
@@ -49,7 +43,6 @@ describe('npm publish auth diagnostics', () => {
49
43
 
50
44
  await expect(
51
45
  publishWithAuthDiagnostics(pkg, shell, {
52
- useBootstrapToken: false,
53
46
  tokenPresent: true,
54
47
  repository: 'smoothbricks/codebase',
55
48
  }),
@@ -66,26 +59,24 @@ describe('npm publish auth diagnostics', () => {
66
59
  expect(shell.logs).toEqual([]);
67
60
  });
68
61
 
69
- it('reports bootstrap token guidance and writes it to the publish summary after first-publish auth failure', async () => {
62
+ it('reports bootstrap command guidance and writes it to the publish summary after first-publish auth failure', async () => {
70
63
  const shell = new RecordingPublishShell({ publishFails: true });
71
64
 
72
- await expect(
73
- publishWithAuthDiagnostics(pkg, shell, { useBootstrapToken: true, tokenPresent: false }),
74
- ).rejects.toThrow('@scope/pkg@1.2.3: npm publish authentication failed');
65
+ await expect(publishWithAuthDiagnostics(pkg, shell, { tokenPresent: false })).rejects.toThrow(
66
+ '@scope/pkg@1.2.3: npm publish authentication failed',
67
+ );
75
68
 
76
69
  expect(shell.errors).toHaveLength(1);
77
- expect(shell.errors[0]).toContain('temporary npm automation token');
78
- expect(shell.errors[0]).toContain('NODE_AUTH_TOKEN/NPM_TOKEN is not set');
79
- expect(shell.errors[0]).toContain('smoo release trust-publisher');
70
+ expect(shell.errors[0]).toContain('smoo release trust-publisher --bootstrap');
80
71
  expect(shell.summaries).toHaveLength(1);
81
- expect(shell.summaries[0]).toContain('Add a temporary `NPM_TOKEN` repository secret');
72
+ expect(shell.summaries[0]).toContain('smoo release trust-publisher --bootstrap');
82
73
  expect(shell.logs).toEqual([]);
83
74
  });
84
75
 
85
76
  it('continues without auth warning when the package version appears on npm after publish failure', async () => {
86
77
  const shell = new RecordingPublishShell({ publishFails: true, versionVisibleAfterFailure: true });
87
78
 
88
- await publishWithAuthDiagnostics(pkg, shell, { useBootstrapToken: false, tokenPresent: true });
79
+ await publishWithAuthDiagnostics(pkg, shell, { tokenPresent: true });
89
80
 
90
81
  expect(shell.errors).toEqual([]);
91
82
  expect(shell.summaries).toEqual([]);