@smoothbricks/cli 0.10.1 → 0.10.3
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 +25 -13
- package/dist/cli.js +24 -0
- package/dist/github-ci/index.d.ts +17 -2
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +145 -14
- package/dist/github-ci/outputs.d.ts +22 -0
- package/dist/github-ci/outputs.d.ts.map +1 -0
- package/dist/github-ci/outputs.js +649 -0
- package/dist/lib/conflict-markers.d.ts.map +1 -1
- package/dist/lib/workspace.d.ts +1 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +2 -1
- package/dist/monorepo/managed-files.d.ts +2 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +50 -5
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +26 -0
- package/dist/monorepo/packs/index.d.ts +3 -0
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +5 -2
- package/dist/monorepo/publish-workflow.d.ts +1 -0
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +240 -2
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +20 -7
- package/dist/nx/index.d.ts +6 -0
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +55 -5
- package/dist/release/github-release.d.ts +5 -2
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +11 -0
- package/dist/release/index.d.ts +19 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +140 -31
- package/dist/release/orchestration.d.ts +8 -1
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +38 -1
- package/dist/wrangler/prepare-env.d.ts.map +1 -1
- package/dist/wrangler/prepare-env.js +1 -1
- package/dist/wrangler/scaffold.d.ts.map +1 -1
- package/dist/wrangler/scaffold.js +1 -1
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +5 -0
- package/managed/raw/tooling/direnv/setup-environment.ts +32 -1
- package/managed/templates/github/actions/cache-nix-devenv/action.yml +12 -10
- package/managed/templates/github/actions/cache-node-modules/action.yml +3 -2
- package/managed/templates/github/actions/cache-nx/action.yml +2 -2
- package/managed/templates/github/actions/save-nix-devenv/action.yml +11 -3
- package/package.json +7 -14
- package/src/cli.ts +30 -4
- package/src/github-ci/index.test.ts +486 -0
- package/src/github-ci/index.ts +177 -16
- package/src/github-ci/outputs.ts +506 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +13 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +234 -5
- package/src/monorepo/ci-workflow.ts +3 -1
- package/src/monorepo/managed-files.test.ts +75 -0
- package/src/monorepo/managed-files.ts +61 -5
- package/src/monorepo/package-policy.test.ts +57 -38
- package/src/monorepo/package-policy.ts +28 -0
- package/src/monorepo/packs/index.test.ts +18 -1
- package/src/monorepo/packs/index.ts +7 -3
- package/src/monorepo/publish-workflow.ts +481 -2
- package/src/monorepo/tool-validation.test.ts +59 -24
- package/src/monorepo/tool-validation.ts +21 -7
- package/src/monorepo/wrangler.test.ts +9 -2
- package/src/nx/index.test.ts +39 -0
- package/src/nx/index.ts +65 -5
- package/src/release/__tests__/fixture-repo.test.ts +18 -16
- package/src/release/__tests__/github-release.test.ts +11 -0
- package/src/release/__tests__/orchestration.test.ts +57 -2
- package/src/release/__tests__/trust-publisher.test.ts +91 -2
- package/src/release/github-release.ts +12 -0
- package/src/release/index.ts +218 -40
- package/src/release/orchestration.ts +55 -3
- package/src/wrangler/prepare-env.ts +14 -4
- package/src/wrangler/scaffold.ts +8 -3
|
@@ -12,12 +12,13 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const registryVersions: Record<string, string[]> = {
|
|
14
14
|
'@biomejs/biome': ['2.3.5'],
|
|
15
|
-
'@nx/js': ['
|
|
16
|
-
'@smoothbricks/nx-plugin': ['0.0
|
|
15
|
+
'@nx/js': ['23.1.0'],
|
|
16
|
+
'@smoothbricks/nx-plugin': ['0.3.0'],
|
|
17
17
|
eslint: ['9.39.1'],
|
|
18
18
|
'eslint-stdout': ['1.1.1', '1.1.2'],
|
|
19
|
-
nx: ['
|
|
19
|
+
nx: ['23.1.0'],
|
|
20
20
|
prettier: ['3.6.1'],
|
|
21
|
+
ttsc: ['0.18.4'],
|
|
21
22
|
typescript: ['5.9.3'],
|
|
22
23
|
};
|
|
23
24
|
|
|
@@ -25,7 +26,7 @@ const realFetch = globalThis.fetch;
|
|
|
25
26
|
|
|
26
27
|
describe('tool configuration validation', () => {
|
|
27
28
|
beforeEach(() => {
|
|
28
|
-
globalThis.fetch = mockRegistryFetch
|
|
29
|
+
globalThis.fetch = mockRegistryFetch;
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
afterEach(() => {
|
|
@@ -67,7 +68,7 @@ describe('tool configuration validation', () => {
|
|
|
67
68
|
expect(rootPackage.devDependencies['@smoothbricks/cli']).toBeUndefined();
|
|
68
69
|
expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe('workspace:*');
|
|
69
70
|
expect(rootPackage.devDependencies['eslint-stdout']).toBe('workspace:*');
|
|
70
|
-
expect(rootPackage.devDependencies.nx).toBe('
|
|
71
|
+
expect(rootPackage.devDependencies.nx).toBe('23.1.0');
|
|
71
72
|
expect(rootPackage.workspaces).toContain('tooling');
|
|
72
73
|
expect(toolingPackage.name).toBe('@smoothbricks/tooling');
|
|
73
74
|
expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe('workspace:*');
|
|
@@ -89,13 +90,14 @@ describe('tool configuration validation', () => {
|
|
|
89
90
|
workspaces: ['packages/*', 'tooling'],
|
|
90
91
|
devDependencies: {
|
|
91
92
|
'@biomejs/biome': '^3.0.0',
|
|
92
|
-
'@nx/js': '
|
|
93
|
-
'@smoothbricks/nx-plugin': '^0.
|
|
93
|
+
'@nx/js': '23.2.0',
|
|
94
|
+
'@smoothbricks/nx-plugin': '^0.4.0',
|
|
94
95
|
'@types/bun': '1.3.99',
|
|
95
96
|
eslint: '^10.0.0',
|
|
96
97
|
'eslint-stdout': await currentEslintStdoutRange(),
|
|
97
|
-
nx: '
|
|
98
|
+
nx: '24.0.0',
|
|
98
99
|
prettier: '^3.7.0',
|
|
100
|
+
ttsc: '^0.19.0',
|
|
99
101
|
typescript: '^6.0.0',
|
|
100
102
|
},
|
|
101
103
|
});
|
|
@@ -132,6 +134,35 @@ describe('tool configuration validation', () => {
|
|
|
132
134
|
}
|
|
133
135
|
});
|
|
134
136
|
|
|
137
|
+
it('preserves an installable CLI range when the running CLI is a linked prerelease', async () => {
|
|
138
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
|
|
139
|
+
try {
|
|
140
|
+
const configuredRange = '^0.10.1';
|
|
141
|
+
const runningRange = await currentCliRange();
|
|
142
|
+
const expectedRange = runningRange.includes('-') ? configuredRange : runningRange;
|
|
143
|
+
await writeJson(join(root, 'package.json'), {
|
|
144
|
+
name: '@fixture/app',
|
|
145
|
+
version: '0.0.0',
|
|
146
|
+
private: true,
|
|
147
|
+
workspaces: ['packages/*', 'tooling'],
|
|
148
|
+
});
|
|
149
|
+
await writeJson(join(root, 'tooling/package.json'), {
|
|
150
|
+
name: '@fixture/tooling',
|
|
151
|
+
private: true,
|
|
152
|
+
dependencies: { '@smoothbricks/cli': configuredRange },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const context = readToolContext(root);
|
|
156
|
+
expect(context.policy.cliDependencyRange).toBe(expectedRange);
|
|
157
|
+
applyToolingPackageDefaults(root, context.policy);
|
|
158
|
+
|
|
159
|
+
const toolingPackage = JSON.parse(await readFile(join(root, 'tooling/package.json'), 'utf8'));
|
|
160
|
+
expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe(expectedRange);
|
|
161
|
+
} finally {
|
|
162
|
+
await rm(root, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
135
166
|
it('uses workspace smoo in the SmoothBricks codebase repo', async () => {
|
|
136
167
|
const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
|
|
137
168
|
try {
|
|
@@ -161,10 +192,11 @@ describe('tool configuration validation', () => {
|
|
|
161
192
|
workspaces: ['packages/*', 'tooling'],
|
|
162
193
|
devDependencies: {
|
|
163
194
|
'@biomejs/biome': '^3.0.0',
|
|
164
|
-
'@nx/js': '
|
|
195
|
+
'@nx/js': '23.2.0',
|
|
165
196
|
eslint: '^10.0.0',
|
|
166
|
-
nx: '
|
|
197
|
+
nx: '24.0.0',
|
|
167
198
|
prettier: '^3.7.0',
|
|
199
|
+
ttsc: '^0.19.0',
|
|
168
200
|
typescript: '^6.0.0',
|
|
169
201
|
},
|
|
170
202
|
});
|
|
@@ -195,20 +227,23 @@ async function currentEslintStdoutRange(): Promise<string> {
|
|
|
195
227
|
return `^${latestPatchVersion('eslint-stdout', pkg.version) ?? pkg.version}`;
|
|
196
228
|
}
|
|
197
229
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
230
|
+
const mockRegistryFetch = Object.assign(
|
|
231
|
+
(input: string | URL | Request): Promise<Response> => {
|
|
232
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
233
|
+
const packageName = decodeURIComponent(new URL(url).pathname.slice(1));
|
|
234
|
+
const versions = registryVersions[packageName];
|
|
235
|
+
if (!versions) {
|
|
236
|
+
return Promise.resolve(new Response('{}', { status: 404 }));
|
|
237
|
+
}
|
|
238
|
+
return Promise.resolve(
|
|
239
|
+
new Response(JSON.stringify({ versions: Object.fromEntries(versions.map((version) => [version, {}])) }), {
|
|
240
|
+
status: 200,
|
|
241
|
+
headers: { 'content-type': 'application/json' },
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
},
|
|
245
|
+
{ preconnect: realFetch.preconnect },
|
|
246
|
+
) satisfies typeof fetch;
|
|
212
247
|
|
|
213
248
|
function latestPatchVersion(packageName: string, version: string): string | null {
|
|
214
249
|
const [major, minor] = version.split('.');
|
|
@@ -25,11 +25,11 @@ export interface ToolContext {
|
|
|
25
25
|
|
|
26
26
|
const rootDevDependencies: RequiredDependency[] = [
|
|
27
27
|
{ name: '@biomejs/biome', fallbackVersion: '^2.3.5', minimumVersion: '2.3.0', prefix: '^' },
|
|
28
|
-
{ name: '@nx/js', fallbackVersion: '
|
|
28
|
+
{ name: '@nx/js', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
|
|
29
29
|
{
|
|
30
30
|
name: '@smoothbricks/nx-plugin',
|
|
31
|
-
fallbackVersion: '^0.0
|
|
32
|
-
minimumVersion: '0.0
|
|
31
|
+
fallbackVersion: '^0.3.0',
|
|
32
|
+
minimumVersion: '0.3.0',
|
|
33
33
|
prefix: '^',
|
|
34
34
|
useWorkspaceRangeInCodebase: true,
|
|
35
35
|
},
|
|
@@ -41,8 +41,11 @@ const rootDevDependencies: RequiredDependency[] = [
|
|
|
41
41
|
prefix: '^',
|
|
42
42
|
useWorkspaceRangeInCodebase: true,
|
|
43
43
|
},
|
|
44
|
-
{ name: 'nx', fallbackVersion: '
|
|
44
|
+
{ name: 'nx', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
|
|
45
45
|
{ name: 'prettier', fallbackVersion: '^3.6.1', minimumVersion: '3.6.0', prefix: '^' },
|
|
46
|
+
{ name: 'ttsc', fallbackVersion: '^0.18.4', minimumVersion: '0.18.4', prefix: '^' },
|
|
47
|
+
// Nx and typescript-eslint still load the TypeScript 5.x JavaScript API.
|
|
48
|
+
// Compilation is exclusively delegated to ttsc by the Nx plugin targets.
|
|
46
49
|
{ name: 'typescript', fallbackVersion: '^5.9.3', minimumVersion: '5.9.0', prefix: '^' },
|
|
47
50
|
];
|
|
48
51
|
|
|
@@ -330,17 +333,28 @@ function formatExpectedDependency(policy: ToolPolicy, dependency: RequiredDepend
|
|
|
330
333
|
|
|
331
334
|
export function readToolContext(root: string): ToolContext {
|
|
332
335
|
const rootPackage = readPackageJsonObject(join(root, 'package.json'));
|
|
333
|
-
|
|
336
|
+
const toolingPackage = readJsonObject(join(root, 'tooling', 'package.json'));
|
|
337
|
+
const configuredCliRange = recordProperty(toolingPackage ?? {}, 'dependencies')?.[cliPackageName];
|
|
338
|
+
return {
|
|
339
|
+
rootPackage,
|
|
340
|
+
policy: toolPolicy(rootPackage, typeof configuredCliRange === 'string' ? configuredCliRange : null),
|
|
341
|
+
};
|
|
334
342
|
}
|
|
335
343
|
|
|
336
|
-
function toolPolicy(rootPackage: PackageJson | null): ToolPolicy {
|
|
344
|
+
function toolPolicy(rootPackage: PackageJson | null, configuredCliRange: string | null): ToolPolicy {
|
|
337
345
|
const name = typeof rootPackage?.name === 'string' ? rootPackage.name : null;
|
|
338
346
|
const isCodebase = isSmoothBricksCodebasePackageName(name ?? undefined);
|
|
339
347
|
const toolingName = toolingPackageName(name);
|
|
348
|
+
// A locally linked prerelease can be newer than every published CLI. Keep an existing
|
|
349
|
+
// installable range instead of rewriting the consumer manifest to an unpublished version.
|
|
350
|
+
const publishedCliRange =
|
|
351
|
+
cliPackageVersion.includes('-') && configuredCliRange && parseVersion(configuredCliRange)
|
|
352
|
+
? configuredCliRange
|
|
353
|
+
: `^${cliPackageVersion}`;
|
|
340
354
|
return {
|
|
341
355
|
isSmoothBricksCodebase: isCodebase,
|
|
342
356
|
toolingPackageName: toolingName,
|
|
343
|
-
cliDependencyRange: isCodebase ? 'workspace:*' :
|
|
357
|
+
cliDependencyRange: isCodebase ? 'workspace:*' : publishedCliRange,
|
|
344
358
|
};
|
|
345
359
|
}
|
|
346
360
|
|
|
@@ -21,7 +21,12 @@ describe('firstWranglerEnv', () => {
|
|
|
21
21
|
describe('applyWranglerDefaults', () => {
|
|
22
22
|
it('injects the wrangler-types target and wires typecheck, idempotently', async () => {
|
|
23
23
|
const root = await createWorkspace([
|
|
24
|
-
{
|
|
24
|
+
{
|
|
25
|
+
dir: 'api',
|
|
26
|
+
name: '@acme/api',
|
|
27
|
+
toml: '[env.production]\n',
|
|
28
|
+
nx: { targets: { typecheck: { dependsOn: ['^build'] } } },
|
|
29
|
+
},
|
|
25
30
|
]);
|
|
26
31
|
try {
|
|
27
32
|
applyWranglerDefaults(root);
|
|
@@ -51,7 +56,9 @@ describe('applyWranglerDefaults', () => {
|
|
|
51
56
|
});
|
|
52
57
|
|
|
53
58
|
it('omits --env from the command when the wrangler.toml declares no env block', async () => {
|
|
54
|
-
const root = await createWorkspace([
|
|
59
|
+
const root = await createWorkspace([
|
|
60
|
+
{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\nmain = "src/index.ts"\n' },
|
|
61
|
+
]);
|
|
55
62
|
captureConsoleLogs();
|
|
56
63
|
try {
|
|
57
64
|
applyWranglerDefaults(root);
|
package/src/nx/index.test.ts
CHANGED
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
nxShowProjectCommand,
|
|
8
8
|
projectNamesFromNxShowProjectsOutput,
|
|
9
9
|
projectNamesWithTarget,
|
|
10
|
+
projectRootFromNxProjectJson,
|
|
11
|
+
targetDependenciesFromNxProjectJson,
|
|
10
12
|
targetNamesFromNxProjectJson,
|
|
13
|
+
targetOutputsFromNxProjectJson,
|
|
11
14
|
} from './index.js';
|
|
12
15
|
|
|
13
16
|
describe('Nx helper command construction', () => {
|
|
@@ -67,6 +70,42 @@ describe('Nx helper output formatting', () => {
|
|
|
67
70
|
]);
|
|
68
71
|
});
|
|
69
72
|
|
|
73
|
+
it('extracts resolved roots, output declarations, and same-project target dependencies', () => {
|
|
74
|
+
const metadata = {
|
|
75
|
+
root: 'packages/native',
|
|
76
|
+
targets: {
|
|
77
|
+
'build-macos': {
|
|
78
|
+
outputs: ['{projectRoot}/dist/*.dmg'],
|
|
79
|
+
dependsOn: ['^build', 'compile-macos', { target: 'package-macos', projects: 'self' }],
|
|
80
|
+
},
|
|
81
|
+
test: { outputs: [] },
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
expect(projectRootFromNxProjectJson(metadata)).toBe('packages/native');
|
|
86
|
+
expect(targetOutputsFromNxProjectJson(metadata)).toEqual(
|
|
87
|
+
new Map([
|
|
88
|
+
['build-macos', ['{projectRoot}/dist/*.dmg']],
|
|
89
|
+
['test', []],
|
|
90
|
+
]),
|
|
91
|
+
);
|
|
92
|
+
expect(targetDependenciesFromNxProjectJson(metadata)).toEqual(
|
|
93
|
+
new Map([['build-macos', ['^build', 'compile-macos', 'package-macos']]]),
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects unsupported cross-project object target dependencies', () => {
|
|
98
|
+
expect(() =>
|
|
99
|
+
targetDependenciesFromNxProjectJson({
|
|
100
|
+
targets: {
|
|
101
|
+
build: {
|
|
102
|
+
dependsOn: [{ target: 'build', projects: 'dependencies' }],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
).toThrow('unsupported cross-project dependsOn');
|
|
107
|
+
});
|
|
108
|
+
|
|
70
109
|
it('treats missing target metadata as an empty project', () => {
|
|
71
110
|
expect(targetNamesFromNxProjectJson({ name: 'cli' })).toEqual([]);
|
|
72
111
|
});
|
package/src/nx/index.ts
CHANGED
|
@@ -7,9 +7,12 @@ import { decode, run } from '../lib/run.js';
|
|
|
7
7
|
|
|
8
8
|
export interface ProjectTargets {
|
|
9
9
|
project: string;
|
|
10
|
+
root?: string;
|
|
10
11
|
targets: string[];
|
|
11
12
|
buildDependsOn?: string[];
|
|
13
|
+
targetDependencies?: Map<string, string[]>;
|
|
12
14
|
targetExecutors?: Map<string, string>;
|
|
15
|
+
targetOutputs?: Map<string, string[]>;
|
|
13
16
|
targetScripts?: Map<string, string>;
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -35,13 +38,41 @@ export function targetNamesFromNxProjectJson(value: unknown): string[] {
|
|
|
35
38
|
return targets ? Object.keys(targets).sort((a, b) => a.localeCompare(b)) : [];
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
export function projectRootFromNxProjectJson(value: unknown): string | undefined {
|
|
42
|
+
return isRecord(value) && typeof value.root === 'string' ? value.root : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
export function buildDependsOnFromNxProjectJson(value: unknown): string[] | undefined {
|
|
46
|
+
return targetDependenciesFromNxProjectJson(value).get('build');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function targetDependenciesFromNxProjectJson(value: unknown): Map<string, string[]> {
|
|
39
50
|
const targets = isRecord(value) ? recordProperty(value, 'targets') : null;
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
42
|
-
return
|
|
51
|
+
const dependencies = new Map<string, string[]>();
|
|
52
|
+
if (!targets) {
|
|
53
|
+
return dependencies;
|
|
54
|
+
}
|
|
55
|
+
for (const [targetName, target] of Object.entries(targets)) {
|
|
56
|
+
if (!isRecord(target) || !Array.isArray(target.dependsOn)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const entries: string[] = [];
|
|
60
|
+
for (const dependency of target.dependsOn) {
|
|
61
|
+
if (typeof dependency === 'string') {
|
|
62
|
+
entries.push(dependency);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!isRecord(dependency) || typeof dependency.target !== 'string') {
|
|
66
|
+
throw new Error(`Nx target ${targetName} has an invalid dependsOn entry.`);
|
|
67
|
+
}
|
|
68
|
+
if (dependency.projects !== undefined && dependency.projects !== 'self') {
|
|
69
|
+
throw new Error(`Nx target ${targetName} uses unsupported cross-project dependsOn for ${dependency.target}.`);
|
|
70
|
+
}
|
|
71
|
+
entries.push(dependency.target);
|
|
72
|
+
}
|
|
73
|
+
dependencies.set(targetName, entries);
|
|
43
74
|
}
|
|
44
|
-
return
|
|
75
|
+
return dependencies;
|
|
45
76
|
}
|
|
46
77
|
|
|
47
78
|
export function targetExecutorsFromNxProjectJson(value: unknown): Map<string, string> {
|
|
@@ -58,6 +89,10 @@ export function targetExecutorsFromNxProjectJson(value: unknown): Map<string, st
|
|
|
58
89
|
return executors;
|
|
59
90
|
}
|
|
60
91
|
|
|
92
|
+
export function targetOutputsFromNxProjectJson(value: unknown): Map<string, string[]> {
|
|
93
|
+
return targetStringArraysFromNxProjectJson(value, 'outputs');
|
|
94
|
+
}
|
|
95
|
+
|
|
61
96
|
export function targetScriptsFromNxProjectJson(value: unknown): Map<string, string> {
|
|
62
97
|
const targets = isRecord(value) ? recordProperty(value, 'targets') : null;
|
|
63
98
|
const scripts = new Map<string, string>();
|
|
@@ -76,6 +111,27 @@ export function targetScriptsFromNxProjectJson(value: unknown): Map<string, stri
|
|
|
76
111
|
return scripts;
|
|
77
112
|
}
|
|
78
113
|
|
|
114
|
+
function targetStringArraysFromNxProjectJson(value: unknown, property: string): Map<string, string[]> {
|
|
115
|
+
const targets = isRecord(value) ? recordProperty(value, 'targets') : null;
|
|
116
|
+
const values = new Map<string, string[]>();
|
|
117
|
+
if (!targets) {
|
|
118
|
+
return values;
|
|
119
|
+
}
|
|
120
|
+
for (const [targetName, target] of Object.entries(targets)) {
|
|
121
|
+
if (!isRecord(target)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const entries = target[property];
|
|
125
|
+
if (Array.isArray(entries)) {
|
|
126
|
+
values.set(
|
|
127
|
+
targetName,
|
|
128
|
+
entries.filter((entry): entry is string => typeof entry === 'string'),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return values;
|
|
133
|
+
}
|
|
134
|
+
|
|
79
135
|
export function formatProjectTargetLines(projects: ProjectTargets[]): string {
|
|
80
136
|
return projects
|
|
81
137
|
.flatMap((project) => project.targets.map((target) => `${project.project}:${target}`))
|
|
@@ -165,11 +221,15 @@ async function readProjectTarget(root: string, project: string): Promise<Project
|
|
|
165
221
|
const command = nxShowProjectCommand(project);
|
|
166
222
|
const result = await $`${command.command} ${command.args}`.cwd(root).quiet();
|
|
167
223
|
const parsed: unknown = JSON.parse(decode(result.stdout));
|
|
224
|
+
const targetDependencies = targetDependenciesFromNxProjectJson(parsed);
|
|
168
225
|
return {
|
|
169
226
|
project,
|
|
227
|
+
root: projectRootFromNxProjectJson(parsed),
|
|
170
228
|
targets: targetNamesFromNxProjectJson(parsed),
|
|
171
|
-
buildDependsOn:
|
|
229
|
+
buildDependsOn: targetDependencies.get('build'),
|
|
230
|
+
targetDependencies,
|
|
172
231
|
targetExecutors: targetExecutorsFromNxProjectJson(parsed),
|
|
232
|
+
targetOutputs: targetOutputsFromNxProjectJson(parsed),
|
|
173
233
|
targetScripts: targetScriptsFromNxProjectJson(parsed),
|
|
174
234
|
};
|
|
175
235
|
}
|
|
@@ -175,7 +175,9 @@ describe('release planning with fixture git repositories', () => {
|
|
|
175
175
|
const summaries = await repairPendingTargets(shell, pending, restoreRef, false);
|
|
176
176
|
|
|
177
177
|
expect(shell.checkouts).toEqual([githubOnlySha, npmAndGithubSha, restoreRef]);
|
|
178
|
-
expect(shell.devenvLoads).toBe(
|
|
178
|
+
expect(shell.devenvLoads).toBe(3);
|
|
179
|
+
expect(shell.devenvRefs).toEqual([githubOnlySha, npmAndGithubSha, headSha]);
|
|
180
|
+
await expect(readFile(join(runner, '.generated-tool-ref'), 'utf8')).resolves.toBe(`${headSha}\n`);
|
|
179
181
|
expect(shell.builds).toEqual([['@scope/b']]);
|
|
180
182
|
expect(shell.publishes).toEqual([{ name: '@scope/b', version: '2.0.0-beta.1', distTag: 'next', dryRun: false }]);
|
|
181
183
|
expect(shell.githubCreates).toEqual([
|
|
@@ -189,40 +191,36 @@ describe('release planning with fixture git repositories', () => {
|
|
|
189
191
|
});
|
|
190
192
|
});
|
|
191
193
|
|
|
192
|
-
it('pushes current release refs to a local bare remote
|
|
194
|
+
it('pushes current release refs to a local bare remote', async () => {
|
|
193
195
|
await withFixtureRepo(async (author) => {
|
|
194
196
|
await writeWorkspace(author);
|
|
195
197
|
await writeBuildablePackage(author, '@scope/pushed', 'packages/pushed', '1.0.0');
|
|
196
198
|
await git(author, ['add', '.']);
|
|
197
199
|
await git(author, ['commit', '-m', 'release pushed package']);
|
|
200
|
+
const releaseSha = await gitOutput(author, ['rev-parse', 'HEAD']);
|
|
198
201
|
await git(author, ['init', '--bare', 'remote.git']);
|
|
199
202
|
await git(author, ['remote', 'add', 'origin', join(author, 'remote.git')]);
|
|
200
203
|
await git(author, ['push', 'origin', 'main']);
|
|
201
|
-
await git(author, ['clone', '--branch', 'main', join(author, 'remote.git'), 'runner']);
|
|
202
|
-
const runner = join(author, 'runner');
|
|
203
|
-
await git(runner, ['config', 'user.name', 'Test User']);
|
|
204
|
-
await git(runner, ['config', 'user.email', 'test@example.com']);
|
|
205
204
|
const pkg: ReleasePackageInfo = {
|
|
206
205
|
name: '@scope/pushed',
|
|
207
206
|
projectName: 'pushed',
|
|
208
207
|
path: 'packages/pushed',
|
|
209
208
|
version: '1.0.0',
|
|
210
209
|
};
|
|
211
|
-
const shell = new LocalGitRepairShell(
|
|
210
|
+
const shell = new LocalGitRepairShell(author);
|
|
212
211
|
|
|
213
212
|
const summary = await completeReleaseAtHead(shell, [pkg], false, false);
|
|
214
213
|
|
|
215
214
|
expect(summary.pushed).toBe(true);
|
|
216
215
|
expect(shell.pushes).toEqual([['pushed@1.0.0']]);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
);
|
|
216
|
+
const remoteTags = await gitOutput(author, [
|
|
217
|
+
'ls-remote',
|
|
218
|
+
'--tags',
|
|
219
|
+
'origin',
|
|
220
|
+
'refs/tags/pushed@1.0.0^{}',
|
|
221
|
+
'refs/tags/@scope/pushed@1.0.0^{}',
|
|
222
|
+
]);
|
|
223
|
+
expect(remoteTags).toBe(`${releaseSha}\trefs/tags/pushed@1.0.0^{}`);
|
|
226
224
|
});
|
|
227
225
|
});
|
|
228
226
|
|
|
@@ -295,6 +293,7 @@ class LocalGitRepairShell implements ReleaseRepairShell<ReleasePackageInfo> {
|
|
|
295
293
|
readonly publishes: Array<{ name: string; version: string; distTag: string; dryRun: boolean }> = [];
|
|
296
294
|
readonly githubCreates: Array<{ name: string; version: string; dryRun: boolean }> = [];
|
|
297
295
|
devenvLoads = 0;
|
|
296
|
+
readonly devenvRefs: string[] = [];
|
|
298
297
|
|
|
299
298
|
constructor(private readonly root: string) {}
|
|
300
299
|
|
|
@@ -360,6 +359,9 @@ class LocalGitRepairShell implements ReleaseRepairShell<ReleasePackageInfo> {
|
|
|
360
359
|
|
|
361
360
|
async withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T> {
|
|
362
361
|
this.devenvLoads += 1;
|
|
362
|
+
const currentRef = await this.gitHead();
|
|
363
|
+
this.devenvRefs.push(currentRef);
|
|
364
|
+
await writeFile(join(this.root, '.generated-tool-ref'), `${currentRef}\n`);
|
|
363
365
|
return runWithEnv();
|
|
364
366
|
}
|
|
365
367
|
|
|
@@ -4,6 +4,7 @@ import type { ReleasePackageInfo } from '../core.js';
|
|
|
4
4
|
import {
|
|
5
5
|
createOrUpdateGithubRelease,
|
|
6
6
|
type GithubReleaseWriteShell,
|
|
7
|
+
githubReleaseLookupExists,
|
|
7
8
|
nxProjectChangelogArgs,
|
|
8
9
|
projectChangelogContents,
|
|
9
10
|
} from '../github-release.js';
|
|
@@ -26,6 +27,7 @@ describe('GitHub release helpers', () => {
|
|
|
26
27
|
gitPush: false,
|
|
27
28
|
stageChanges: false,
|
|
28
29
|
createRelease: false,
|
|
30
|
+
forceChangelogGeneration: true,
|
|
29
31
|
deleteVersionPlans: false,
|
|
30
32
|
dryRun: false,
|
|
31
33
|
from: 'pkg@1.2.2',
|
|
@@ -41,6 +43,7 @@ describe('GitHub release helpers', () => {
|
|
|
41
43
|
gitPush: false,
|
|
42
44
|
stageChanges: false,
|
|
43
45
|
createRelease: false,
|
|
46
|
+
forceChangelogGeneration: true,
|
|
44
47
|
deleteVersionPlans: false,
|
|
45
48
|
dryRun: true,
|
|
46
49
|
firstRelease: true,
|
|
@@ -66,6 +69,14 @@ describe('GitHub release helpers', () => {
|
|
|
66
69
|
);
|
|
67
70
|
});
|
|
68
71
|
|
|
72
|
+
it('distinguishes missing releases from transient GitHub lookup failures', () => {
|
|
73
|
+
expect(githubReleaseLookupExists('pkg@1.2.3', 0, '{"tagName":"pkg@1.2.3"}', '')).toBe(true);
|
|
74
|
+
expect(githubReleaseLookupExists('pkg@1.2.3', 1, '', 'HTTP 404: release not found')).toBe(false);
|
|
75
|
+
expect(() => githubReleaseLookupExists('pkg@1.2.3', 1, '', 'HTTP 503: Service Unavailable')).toThrow(
|
|
76
|
+
'Unable to inspect GitHub Release pkg@1.2.3.\nHTTP 503: Service Unavailable',
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
69
80
|
it('creates a missing stable GitHub Release from the generated notes file', async () => {
|
|
70
81
|
const shell = new RecordingGithubReleaseShell(false);
|
|
71
82
|
|
|
@@ -2,7 +2,9 @@ import { describe, expect, it } from 'bun:test';
|
|
|
2
2
|
import { type ReleasePackageInfo, type ReleaseTarget, releaseTag } from '../core.js';
|
|
3
3
|
import {
|
|
4
4
|
bumpStableReleaseToNext,
|
|
5
|
+
collectRepairPlatformOutputs,
|
|
5
6
|
completeReleaseAtHead,
|
|
7
|
+
type ReleaseRepairOutputsShell,
|
|
6
8
|
type ReleaseRepairShell,
|
|
7
9
|
type ReleaseVersionShell,
|
|
8
10
|
repairPendingTargets,
|
|
@@ -23,14 +25,16 @@ const prerelease: ReleasePackageInfo = {
|
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
describe('release orchestration', () => {
|
|
26
|
-
it('repairs an older target
|
|
28
|
+
it('repairs an older target and refreshes devenv after restoring the caller ref', async () => {
|
|
27
29
|
const target = releaseTarget('older-release', [stable, prerelease], [stable], [prerelease]);
|
|
28
30
|
const shell = new RecordingRepairShell();
|
|
29
31
|
|
|
30
32
|
const summaries = await repairPendingTargets(shell, [target], 'restore-ref', false);
|
|
31
33
|
|
|
32
34
|
expect(shell.checkouts).toEqual(['older-release', 'restore-ref']);
|
|
33
|
-
expect(shell.devenvLoads).toBe(
|
|
35
|
+
expect(shell.devenvLoads).toBe(2);
|
|
36
|
+
expect(shell.devenvRefs).toEqual(['older-release', 'restore-ref']);
|
|
37
|
+
expect(shell.prepares).toEqual(['older-release']);
|
|
34
38
|
expect(shell.builds).toEqual([['@scope/stable']]);
|
|
35
39
|
expect(shell.publishes).toEqual([{ name: '@scope/stable', distTag: 'latest', dryRun: false }]);
|
|
36
40
|
expect(shell.githubCreates).toEqual([{ name: '@scope/prerelease', dryRun: false }]);
|
|
@@ -78,6 +82,29 @@ describe('release orchestration', () => {
|
|
|
78
82
|
]);
|
|
79
83
|
});
|
|
80
84
|
|
|
85
|
+
it('collects historical platform outputs only for npm-missing repair targets', async () => {
|
|
86
|
+
const githubOnly = releaseTarget('github-only', [prerelease], [], [prerelease]);
|
|
87
|
+
const npmMissing = releaseTarget('npm-missing', [stable], [stable], []);
|
|
88
|
+
const shell = new RecordingRepairOutputsShell();
|
|
89
|
+
|
|
90
|
+
await collectRepairPlatformOutputs(shell, [githubOnly, npmMissing], 'restore-ref');
|
|
91
|
+
|
|
92
|
+
expect(shell.checkouts).toEqual(['npm-missing', 'restore-ref']);
|
|
93
|
+
expect(shell.devenvRefs).toEqual(['npm-missing', 'restore-ref']);
|
|
94
|
+
expect(shell.collected).toEqual(['npm-missing']);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('does nothing when no pending release needs npm platform outputs', async () => {
|
|
98
|
+
const shell = new RecordingRepairOutputsShell();
|
|
99
|
+
const githubOnly = releaseTarget('github-only', [prerelease], [], [prerelease]);
|
|
100
|
+
|
|
101
|
+
await collectRepairPlatformOutputs(shell, [githubOnly], 'restore-ref');
|
|
102
|
+
|
|
103
|
+
expect(shell.checkouts).toEqual([]);
|
|
104
|
+
expect(shell.devenvRefs).toEqual([]);
|
|
105
|
+
expect(shell.collected).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
81
108
|
it('publishes a partial HEAD release by building npm-missing packages and creating missing GitHub Releases', async () => {
|
|
82
109
|
const shell = new RecordingRepairShell({ npmMissing: ['@scope/stable'], githubMissing: ['@scope/prerelease'] });
|
|
83
110
|
|
|
@@ -216,15 +243,21 @@ class RecordingRepairShell implements ReleaseRepairShell<ReleasePackageInfo> {
|
|
|
216
243
|
readonly npmQueries: string[][] = [];
|
|
217
244
|
readonly githubQueries: string[][] = [];
|
|
218
245
|
devenvLoads = 0;
|
|
246
|
+
readonly devenvRefs: string[] = [];
|
|
219
247
|
currentRef = 'head';
|
|
220
248
|
private readonly npmMissing: Set<string>;
|
|
221
249
|
private readonly githubMissing: Set<string>;
|
|
250
|
+
readonly prepares: string[] = [];
|
|
222
251
|
|
|
223
252
|
constructor(options: { npmMissing?: string[]; githubMissing?: string[] } = {}) {
|
|
224
253
|
this.npmMissing = new Set(options.npmMissing ?? []);
|
|
225
254
|
this.githubMissing = new Set(options.githubMissing ?? []);
|
|
226
255
|
}
|
|
227
256
|
|
|
257
|
+
async prepareRepairTarget(target: ReleaseTarget<ReleasePackageInfo>): Promise<void> {
|
|
258
|
+
this.prepares.push(target.sha);
|
|
259
|
+
}
|
|
260
|
+
|
|
228
261
|
async gitHead(): Promise<string> {
|
|
229
262
|
return this.currentRef;
|
|
230
263
|
}
|
|
@@ -264,10 +297,32 @@ class RecordingRepairShell implements ReleaseRepairShell<ReleasePackageInfo> {
|
|
|
264
297
|
|
|
265
298
|
async withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T> {
|
|
266
299
|
this.devenvLoads += 1;
|
|
300
|
+
this.devenvRefs.push(this.currentRef);
|
|
267
301
|
return runWithEnv();
|
|
268
302
|
}
|
|
269
303
|
}
|
|
270
304
|
|
|
305
|
+
class RecordingRepairOutputsShell implements ReleaseRepairOutputsShell<ReleasePackageInfo> {
|
|
306
|
+
readonly checkouts: string[] = [];
|
|
307
|
+
readonly collected: string[] = [];
|
|
308
|
+
readonly devenvRefs: string[] = [];
|
|
309
|
+
private currentRef = 'head';
|
|
310
|
+
|
|
311
|
+
async checkout(ref: string): Promise<void> {
|
|
312
|
+
this.currentRef = ref;
|
|
313
|
+
this.checkouts.push(ref);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T> {
|
|
317
|
+
this.devenvRefs.push(this.currentRef);
|
|
318
|
+
return runWithEnv();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async collectRepairTargetOutputs(target: ReleaseTarget<ReleasePackageInfo>): Promise<void> {
|
|
322
|
+
this.collected.push(target.sha);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
271
326
|
class RecordingVersionShell implements ReleaseVersionShell<ReleasePackageInfo> {
|
|
272
327
|
readonly ensureCalls: string[][] = [];
|
|
273
328
|
readonly nxRuns: Array<{ packages: string[]; bump: string; dryRun: boolean }> = [];
|