@smoothbricks/cli 0.10.2 → 0.10.4
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 +31 -23
- package/dist/bun/preload.d.ts +13 -0
- package/dist/bun/preload.d.ts.map +1 -0
- package/dist/bun/preload.js +12 -0
- package/dist/cli.js +36 -4
- package/dist/generate/index.d.ts.map +1 -1
- package/dist/generate/index.js +0 -6
- 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 +192 -26
- 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 +648 -0
- package/dist/lib/cli-package.js +2 -2
- package/dist/lib/json.d.ts +118 -11
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +317 -29
- package/dist/lib/workspace.d.ts +7 -14
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +76 -82
- package/dist/monorepo/ci-workflow.d.ts +2 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +28 -2
- package/dist/monorepo/git-config.js +2 -2
- package/dist/monorepo/index.d.ts +3 -1
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +6 -2
- package/dist/monorepo/lockfile.d.ts +13 -1
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +33 -21
- package/dist/monorepo/managed-files.d.ts +12 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +86 -28
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +78 -45
- package/dist/monorepo/packed-manifest.d.ts +5 -2
- package/dist/monorepo/packed-manifest.d.ts.map +1 -1
- package/dist/monorepo/packed-manifest.js +16 -47
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +28 -16
- 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 +18 -6
- 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 +242 -3
- package/dist/monorepo/runtime.d.ts +15 -0
- package/dist/monorepo/runtime.d.ts.map +1 -1
- package/dist/monorepo/runtime.js +66 -12
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +32 -23
- package/dist/monorepo/wrangler.d.ts.map +1 -1
- package/dist/monorepo/wrangler.js +13 -13
- package/dist/nx/index.d.ts +12 -4
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +86 -19
- package/dist/pr/index.d.ts.map +1 -1
- package/dist/pr/index.js +28 -32
- package/dist/release/candidates.d.ts +3 -2
- package/dist/release/candidates.d.ts.map +1 -1
- package/dist/release/candidates.js +4 -5
- package/dist/release/github-release.d.ts +3 -0
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +11 -0
- package/dist/release/index.d.ts +20 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +221 -89
- 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 +0 -1
- package/dist/wrangler/prepare-env.d.ts.map +1 -1
- package/dist/wrangler/prepare-env.js +100 -51
- package/dist/wrangler/scaffold.d.ts.map +1 -1
- package/dist/wrangler/scaffold.js +12 -12
- package/managed/raw/git-format-staged.yml +19 -1
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +6 -0
- package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
- package/managed/raw/tooling/direnv/setup-environment.ts +187 -19
- package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
- package/managed/templates/github/actions/cache-nix-devenv/action.yml +12 -10
- package/managed/templates/github/actions/cache-node-modules/action.yml +6 -2
- package/managed/templates/github/actions/cache-nx/action.yml +2 -2
- package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
- package/managed/templates/github/actions/save-nix-devenv/action.yml +11 -3
- package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
- package/managed/templates/github/workflows/managed-files.yml +80 -0
- package/package.json +18 -4
- package/src/bun/preload.ts +12 -0
- package/src/cli.ts +49 -11
- package/src/generate/index.ts +0 -6
- package/src/github-ci/index.test.ts +486 -0
- package/src/github-ci/index.ts +220 -30
- package/src/github-ci/outputs.ts +505 -0
- package/src/lib/cli-package.ts +2 -2
- package/src/lib/json.ts +291 -32
- package/src/lib/workspace.ts +88 -100
- package/src/monorepo/__tests__/ci-workflow.test.ts +14 -3
- package/src/monorepo/__tests__/publish-workflow.test.ts +222 -1
- package/src/monorepo/ci-workflow.ts +28 -2
- package/src/monorepo/git-config.ts +2 -2
- package/src/monorepo/index.ts +6 -2
- package/src/monorepo/lockfile.test.ts +35 -36
- package/src/monorepo/lockfile.ts +48 -24
- package/src/monorepo/managed-files.test.ts +63 -0
- package/src/monorepo/managed-files.ts +94 -34
- package/src/monorepo/package-policy.test.ts +70 -51
- package/src/monorepo/package-policy.ts +104 -59
- package/src/monorepo/packed-manifest.ts +18 -52
- package/src/monorepo/packed-package.ts +43 -22
- package/src/monorepo/packs/index.test.ts +18 -1
- package/src/monorepo/packs/index.ts +20 -7
- package/src/monorepo/publish-workflow.ts +482 -3
- package/src/monorepo/runtime.test.ts +42 -3
- package/src/monorepo/runtime.ts +90 -12
- package/src/monorepo/tool-validation.ts +35 -25
- package/src/monorepo/wrangler.test.ts +11 -15
- package/src/monorepo/wrangler.ts +15 -15
- package/src/nx/index.test.ts +43 -0
- package/src/nx/index.ts +110 -23
- package/src/pr/index.ts +42 -36
- package/src/release/__tests__/candidates.test.ts +5 -4
- 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 +98 -2
- package/src/release/candidates.ts +10 -17
- package/src/release/github-release.ts +12 -0
- package/src/release/index.ts +321 -112
- package/src/release/orchestration.ts +55 -3
- package/src/wrangler/prepare-env.ts +75 -54
- package/src/wrangler/scaffold.test.ts +9 -12
- package/src/wrangler/scaffold.ts +13 -13
package/src/nx/index.ts
CHANGED
|
@@ -2,14 +2,23 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { lstat, rm } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { $ } from 'bun';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type NxDependsOn,
|
|
7
|
+
type NxProjectJson,
|
|
8
|
+
type NxTargetConfig,
|
|
9
|
+
parseNxProjectJsonText,
|
|
10
|
+
parseStringArrayText,
|
|
11
|
+
} from '../lib/json.js';
|
|
6
12
|
import { decode, run } from '../lib/run.js';
|
|
7
13
|
|
|
8
14
|
export interface ProjectTargets {
|
|
9
15
|
project: string;
|
|
16
|
+
root?: string;
|
|
10
17
|
targets: string[];
|
|
11
18
|
buildDependsOn?: string[];
|
|
19
|
+
targetDependencies?: Map<string, string[]>;
|
|
12
20
|
targetExecutors?: Map<string, string>;
|
|
21
|
+
targetOutputs?: Map<string, string[]>;
|
|
13
22
|
targetScripts?: Map<string, string>;
|
|
14
23
|
}
|
|
15
24
|
|
|
@@ -30,52 +39,121 @@ export function nxCacheDirectories(root: string): string[] {
|
|
|
30
39
|
return [join(root, '.nx/cache'), join(root, '.nx/workspace-data'), join(root, 'node_modules/.cache/nx')];
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
export function targetNamesFromNxProjectJson(value:
|
|
34
|
-
const targets =
|
|
42
|
+
export function targetNamesFromNxProjectJson(value: NxProjectJson | null | undefined): string[] {
|
|
43
|
+
const targets = value?.targets;
|
|
35
44
|
return targets ? Object.keys(targets).sort((a, b) => a.localeCompare(b)) : [];
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
export function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
export function projectRootFromNxProjectJson(value: NxProjectJson | null | undefined): string | undefined {
|
|
48
|
+
return typeof value?.root === 'string' ? value.root : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildDependsOnFromNxProjectJson(value: NxProjectJson | null | undefined): string[] | undefined {
|
|
52
|
+
return targetDependenciesFromNxProjectJson(value).get('build');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function targetDependenciesFromNxProjectJson(value: NxProjectJson | null | undefined): Map<string, string[]> {
|
|
56
|
+
const targets = value?.targets;
|
|
57
|
+
const dependencies = new Map<string, string[]>();
|
|
58
|
+
if (!targets) {
|
|
59
|
+
return dependencies;
|
|
60
|
+
}
|
|
61
|
+
for (const [targetName, target] of Object.entries(targets)) {
|
|
62
|
+
if (!target.dependsOn) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const entries: string[] = [];
|
|
66
|
+
for (const dependency of target.dependsOn) {
|
|
67
|
+
const local = localDependsOnTarget(dependency);
|
|
68
|
+
if (local === undefined) {
|
|
69
|
+
if (isInvalidDependsOn(dependency)) {
|
|
70
|
+
throw new Error(`Nx target ${targetName} has an invalid dependsOn entry.`);
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
entries.push(local);
|
|
75
|
+
}
|
|
76
|
+
dependencies.set(targetName, entries);
|
|
77
|
+
}
|
|
78
|
+
return dependencies;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function localDependsOnTarget(dependency: NxDependsOn): string | undefined {
|
|
82
|
+
if (typeof dependency === 'string') {
|
|
83
|
+
return dependency;
|
|
84
|
+
}
|
|
85
|
+
if (typeof dependency.target !== 'string') {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (dependency.projects !== undefined && dependency.projects !== 'self') {
|
|
89
|
+
// Target closures are intentionally project-local. Nx schedules explicit
|
|
90
|
+
// cross-project prerequisites itself; treating them as local targets
|
|
91
|
+
// would collect or verify outputs from the wrong project.
|
|
42
92
|
return undefined;
|
|
43
93
|
}
|
|
44
|
-
return
|
|
94
|
+
return dependency.target;
|
|
45
95
|
}
|
|
46
96
|
|
|
47
|
-
|
|
48
|
-
|
|
97
|
+
function isInvalidDependsOn(dependency: NxDependsOn): boolean {
|
|
98
|
+
return typeof dependency !== 'string' && typeof dependency.target !== 'string';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function targetExecutorsFromNxProjectJson(value: NxProjectJson | null | undefined): Map<string, string> {
|
|
102
|
+
const targets = value?.targets;
|
|
49
103
|
const executors = new Map<string, string>();
|
|
50
104
|
if (!targets) {
|
|
51
105
|
return executors;
|
|
52
106
|
}
|
|
53
107
|
for (const [targetName, target] of Object.entries(targets)) {
|
|
54
|
-
if (
|
|
108
|
+
if (typeof target.executor === 'string') {
|
|
55
109
|
executors.set(targetName, target.executor);
|
|
56
110
|
}
|
|
57
111
|
}
|
|
58
112
|
return executors;
|
|
59
113
|
}
|
|
60
114
|
|
|
61
|
-
export function
|
|
62
|
-
|
|
115
|
+
export function targetOutputsFromNxProjectJson(value: NxProjectJson | null | undefined): Map<string, string[]> {
|
|
116
|
+
return targetStringArraysFromNxProjectJson(value, 'outputs');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function targetScriptsFromNxProjectJson(value: NxProjectJson | null | undefined): Map<string, string> {
|
|
120
|
+
const targets = value?.targets;
|
|
63
121
|
const scripts = new Map<string, string>();
|
|
64
122
|
if (!targets) {
|
|
65
123
|
return scripts;
|
|
66
124
|
}
|
|
67
125
|
for (const [targetName, target] of Object.entries(targets)) {
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
const options = recordProperty(target, 'options');
|
|
72
|
-
if (typeof options?.script === 'string') {
|
|
73
|
-
scripts.set(targetName, options.script);
|
|
126
|
+
if (typeof target.options?.script === 'string') {
|
|
127
|
+
scripts.set(targetName, target.options.script);
|
|
74
128
|
}
|
|
75
129
|
}
|
|
76
130
|
return scripts;
|
|
77
131
|
}
|
|
78
132
|
|
|
133
|
+
function targetStringArraysFromNxProjectJson(
|
|
134
|
+
value: NxProjectJson | null | undefined,
|
|
135
|
+
property: 'outputs' | 'inputs',
|
|
136
|
+
): Map<string, string[]> {
|
|
137
|
+
const targets = value?.targets;
|
|
138
|
+
const values = new Map<string, string[]>();
|
|
139
|
+
if (!targets) {
|
|
140
|
+
return values;
|
|
141
|
+
}
|
|
142
|
+
for (const [targetName, target] of Object.entries(targets)) {
|
|
143
|
+
const entries = target[property];
|
|
144
|
+
if (Array.isArray(entries)) {
|
|
145
|
+
const strings: string[] = [];
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (typeof entry === 'string') {
|
|
148
|
+
strings.push(entry);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
values.set(targetName, strings);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return values;
|
|
155
|
+
}
|
|
156
|
+
|
|
79
157
|
export function formatProjectTargetLines(projects: ProjectTargets[]): string {
|
|
80
158
|
return projects
|
|
81
159
|
.flatMap((project) => project.targets.map((target) => `${project.project}:${target}`))
|
|
@@ -98,8 +176,8 @@ export function projectNamesFromNxShowProjectsOutput(output: string): string[] {
|
|
|
98
176
|
|
|
99
177
|
if (trimmed.startsWith('[')) {
|
|
100
178
|
try {
|
|
101
|
-
const parsed
|
|
102
|
-
if (
|
|
179
|
+
const parsed = parseStringArrayText(trimmed);
|
|
180
|
+
if (parsed) {
|
|
103
181
|
return parsed.sort((a, b) => a.localeCompare(b));
|
|
104
182
|
}
|
|
105
183
|
} catch {
|
|
@@ -164,12 +242,21 @@ async function readNxProjectNames(root: string): Promise<string[]> {
|
|
|
164
242
|
async function readProjectTarget(root: string, project: string): Promise<ProjectTargets> {
|
|
165
243
|
const command = nxShowProjectCommand(project);
|
|
166
244
|
const result = await $`${command.command} ${command.args}`.cwd(root).quiet();
|
|
167
|
-
const parsed
|
|
245
|
+
const parsed = parseNxProjectJsonText(decode(result.stdout));
|
|
246
|
+
if (!parsed) {
|
|
247
|
+
throw new Error(`Unable to inspect Nx project ${project}.`);
|
|
248
|
+
}
|
|
249
|
+
const targetDependencies = targetDependenciesFromNxProjectJson(parsed);
|
|
168
250
|
return {
|
|
169
251
|
project,
|
|
252
|
+
root: projectRootFromNxProjectJson(parsed),
|
|
170
253
|
targets: targetNamesFromNxProjectJson(parsed),
|
|
171
|
-
buildDependsOn:
|
|
254
|
+
buildDependsOn: targetDependencies.get('build'),
|
|
255
|
+
targetDependencies,
|
|
172
256
|
targetExecutors: targetExecutorsFromNxProjectJson(parsed),
|
|
257
|
+
targetOutputs: targetOutputsFromNxProjectJson(parsed),
|
|
173
258
|
targetScripts: targetScriptsFromNxProjectJson(parsed),
|
|
174
259
|
};
|
|
175
260
|
}
|
|
261
|
+
|
|
262
|
+
export type { NxProjectJson, NxTargetConfig };
|
package/src/pr/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
+
import typia from 'typia';
|
|
3
4
|
import { formatMarkerHits, scanRefChangedForMarkers } from '../lib/conflict-markers.js';
|
|
4
|
-
import {
|
|
5
|
+
import { readJson } from '../lib/json.js';
|
|
5
6
|
import { run, runResult } from '../lib/run.js';
|
|
6
7
|
|
|
7
8
|
/** 0 = clean/done, 2 = action required (conflicts to resolve), 1 = usage/error. */
|
|
@@ -50,6 +51,29 @@ interface PrResolveState {
|
|
|
50
51
|
startedAt: string;
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
interface GhPrViewJson {
|
|
55
|
+
number: number;
|
|
56
|
+
url: string;
|
|
57
|
+
headRefName: string;
|
|
58
|
+
baseRefName: string;
|
|
59
|
+
isCrossRepository?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface PrResolveStateJson {
|
|
63
|
+
pr: number;
|
|
64
|
+
url?: string;
|
|
65
|
+
headBranch: string;
|
|
66
|
+
baseBranch: string;
|
|
67
|
+
remote: string;
|
|
68
|
+
crossRepo?: boolean;
|
|
69
|
+
originalBranch?: string;
|
|
70
|
+
originalSha?: string;
|
|
71
|
+
startedAt?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const parseGhPrViewJson = typia.json.createIsParse<GhPrViewJson>();
|
|
75
|
+
const isPrResolveStateJson = typia.createIs<PrResolveStateJson>();
|
|
76
|
+
|
|
53
77
|
/**
|
|
54
78
|
* Agent-first conflict resolution for a GitHub PR.
|
|
55
79
|
*
|
|
@@ -209,25 +233,16 @@ async function prMeta(shell: PrResolveShell, root: string, prArg: string): Promi
|
|
|
209
233
|
if (result.exitCode !== 0) {
|
|
210
234
|
throw new Error(`Could not resolve PR '${prArg}' via gh: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
|
|
211
235
|
}
|
|
212
|
-
const raw
|
|
213
|
-
if (!
|
|
236
|
+
const raw = parseGhPrViewJson(result.stdout);
|
|
237
|
+
if (!raw) {
|
|
214
238
|
throw new Error(`gh pr view returned unexpected JSON for '${prArg}'.`);
|
|
215
239
|
}
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
218
|
-
typeof number !== 'number' ||
|
|
219
|
-
typeof url !== 'string' ||
|
|
220
|
-
typeof headRefName !== 'string' ||
|
|
221
|
-
typeof baseRefName !== 'string'
|
|
222
|
-
) {
|
|
223
|
-
throw new Error(`gh pr view JSON for '${prArg}' is missing expected fields.`);
|
|
224
|
-
}
|
|
225
|
-
const match = /github\.com\/([^/]+\/[^/]+)\/pull\//.exec(url);
|
|
240
|
+
const match = /github\.com\/([^/]+\/[^/]+)\/pull\//.exec(raw.url);
|
|
226
241
|
return {
|
|
227
|
-
number,
|
|
228
|
-
url,
|
|
229
|
-
headBranch: headRefName,
|
|
230
|
-
baseBranch: baseRefName,
|
|
242
|
+
number: raw.number,
|
|
243
|
+
url: raw.url,
|
|
244
|
+
headBranch: raw.headRefName,
|
|
245
|
+
baseBranch: raw.baseRefName,
|
|
231
246
|
crossRepo: raw.isCrossRepository === true,
|
|
232
247
|
nameWithOwner: match ? match[1] : '',
|
|
233
248
|
};
|
|
@@ -326,28 +341,19 @@ function readState(statePath: string): PrResolveState | null {
|
|
|
326
341
|
} catch {
|
|
327
342
|
return null;
|
|
328
343
|
}
|
|
329
|
-
if (!
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
const { pr, url, headBranch, baseBranch, remote, crossRepo, originalBranch, originalSha, startedAt } = raw;
|
|
333
|
-
if (
|
|
334
|
-
typeof pr !== 'number' ||
|
|
335
|
-
typeof headBranch !== 'string' ||
|
|
336
|
-
typeof baseBranch !== 'string' ||
|
|
337
|
-
typeof remote !== 'string'
|
|
338
|
-
) {
|
|
344
|
+
if (!isPrResolveStateJson(raw)) {
|
|
339
345
|
return null;
|
|
340
346
|
}
|
|
341
347
|
return {
|
|
342
|
-
pr,
|
|
343
|
-
url:
|
|
344
|
-
headBranch,
|
|
345
|
-
baseBranch,
|
|
346
|
-
remote,
|
|
347
|
-
crossRepo: crossRepo === true,
|
|
348
|
-
originalBranch:
|
|
349
|
-
originalSha:
|
|
350
|
-
startedAt:
|
|
348
|
+
pr: raw.pr,
|
|
349
|
+
url: raw.url ?? '',
|
|
350
|
+
headBranch: raw.headBranch,
|
|
351
|
+
baseBranch: raw.baseBranch,
|
|
352
|
+
remote: raw.remote,
|
|
353
|
+
crossRepo: raw.crossRepo === true,
|
|
354
|
+
originalBranch: raw.originalBranch ?? '',
|
|
355
|
+
originalSha: raw.originalSha ?? '',
|
|
356
|
+
startedAt: raw.startedAt ?? '',
|
|
351
357
|
};
|
|
352
358
|
}
|
|
353
359
|
|
|
@@ -2,7 +2,8 @@ import { describe, expect, it } from 'bun:test';
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { $ } from 'bun';
|
|
5
|
-
import {
|
|
5
|
+
import type { PackageJson } from '../../lib/json.js';
|
|
6
|
+
import { parsePackageJsonText } from '../../lib/json.js';
|
|
6
7
|
import { type AutoReleaseCandidateShell, autoReleaseCandidatePackages } from '../candidates.js';
|
|
7
8
|
import type { ReleasePackageInfo } from '../core.js';
|
|
8
9
|
import { git, tag, withFixtureRepo, writePackage } from './helpers/fixture-repo.js';
|
|
@@ -309,9 +310,9 @@ function gitCandidateShell(
|
|
|
309
310
|
};
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
function parseJsonObject(text: string):
|
|
313
|
-
const parsed =
|
|
314
|
-
if (!
|
|
313
|
+
function parseJsonObject(text: string): PackageJson {
|
|
314
|
+
const parsed = parsePackageJsonText(text);
|
|
315
|
+
if (!parsed) {
|
|
315
316
|
throw new Error('expected JSON object');
|
|
316
317
|
}
|
|
317
318
|
return parsed;
|
|
@@ -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 }> = [];
|