@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.
- package/README.md +155 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -7
- 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/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 +5 -0
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +13 -0
- package/dist/lib/workspace.d.ts +15 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +25 -2
- 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/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 +22 -2
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +1167 -8
- 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 +5 -12
- 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 +24 -0
- package/dist/nx/index.d.ts.map +1 -0
- package/dist/nx/index.js +94 -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 +1 -0
- package/dist/release/core.d.ts.map +1 -1
- package/dist/release/github-release.js +2 -2
- package/dist/release/index.d.ts +25 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +247 -49
- 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 +2 -1
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +9 -1
- 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 +1 -0
- package/package.json +15 -3
- package/src/cli.ts +134 -20
- package/src/devenv/index.test.ts +49 -0
- package/src/devenv/index.ts +38 -0
- package/src/lib/cli-package.ts +18 -0
- package/src/lib/run.ts +19 -0
- package/src/lib/workspace.ts +41 -2
- 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/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 +902 -0
- package/src/monorepo/package-policy.ts +1393 -7
- 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 +8 -16
- 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 +64 -0
- package/src/nx/index.ts +119 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
- package/src/release/__tests__/candidates.test.ts +217 -0
- package/src/release/__tests__/core-properties.test.ts +4 -4
- package/src/release/__tests__/core-scenarios.test.ts +15 -5
- package/src/release/__tests__/core.test.ts +2 -2
- package/src/release/__tests__/fixture-repo.test.ts +22 -13
- package/src/release/__tests__/github-release.test.ts +13 -8
- 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 +51 -7
- package/src/release/__tests__/publish-plan.test.ts +9 -3
- package/src/release/__tests__/retag-unpublished.test.ts +1 -1
- package/src/release/__tests__/trust-publisher.test.ts +133 -0
- package/src/release/bootstrap-npm-packages.ts +88 -0
- package/src/release/candidates.ts +225 -0
- package/src/release/core.ts +1 -0
- package/src/release/github-release.ts +2 -2
- package/src/release/index.ts +346 -56
- package/src/release/npm-auth.ts +4 -33
- package/src/release/orchestration.ts +12 -2
- package/src/release/publish-plan.ts +1 -1
- 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.
|
|
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 = {
|
|
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(
|
|
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(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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=${'
|
|
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 = {
|
|
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.
|
|
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 = {
|
|
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: ['
|
|
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: ['
|
|
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
|
-
|
|
55
|
+
pkg: { contents: 'generated release notes' },
|
|
51
56
|
},
|
|
52
57
|
},
|
|
53
|
-
'
|
|
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: {} }, '
|
|
60
|
-
'Nx did not generate a project changelog for
|
|
64
|
+
expect(() => projectChangelogContents({ projectChangelogs: {} }, 'pkg')).toThrow(
|
|
65
|
+
'Nx did not generate a project changelog for pkg.',
|
|
61
66
|
);
|
|
62
67
|
});
|
|
63
68
|
|
|
@@ -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('
|
|
36
|
-
const message = npmPublishAuthFailureMessage(pkg, {
|
|
37
|
-
const markdown = npmPublishAuthFailureMarkdown(pkg, {
|
|
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('
|
|
40
|
-
expect(
|
|
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
|
|
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
|
-
|
|
74
|
-
)
|
|
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('
|
|
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('
|
|
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, {
|
|
79
|
+
await publishWithAuthDiagnostics(pkg, shell, { tokenPresent: true });
|
|
89
80
|
|
|
90
81
|
expect(shell.errors).toEqual([]);
|
|
91
82
|
expect(shell.summaries).toEqual([]);
|