@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/lib/workspace.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, relative } from 'node:path';
|
|
3
3
|
import { isSmoothBricksCodebasePackageName } from './cli-package.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
isPackageJson,
|
|
6
|
+
type PackageJson,
|
|
7
|
+
type PackageNxConfig,
|
|
8
|
+
type PackageRepository,
|
|
9
|
+
readJson,
|
|
10
|
+
readJsonObject,
|
|
11
|
+
} from './json.js';
|
|
12
|
+
|
|
13
|
+
export type { PackageJson, PackageNxConfig, PackageRepository };
|
|
5
14
|
|
|
6
15
|
export interface PackageInfo {
|
|
7
16
|
name: string;
|
|
@@ -11,7 +20,7 @@ export interface PackageInfo {
|
|
|
11
20
|
tags: string[];
|
|
12
21
|
path: string;
|
|
13
22
|
packageJsonPath: string;
|
|
14
|
-
json:
|
|
23
|
+
json: PackageJson;
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
export interface WorkspacePackageManifest {
|
|
@@ -20,18 +29,7 @@ export interface WorkspacePackageManifest {
|
|
|
20
29
|
private: boolean;
|
|
21
30
|
path: string;
|
|
22
31
|
packageJsonPath: string;
|
|
23
|
-
json:
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface PackageJson extends Record<string, unknown> {
|
|
27
|
-
name?: string;
|
|
28
|
-
version?: string;
|
|
29
|
-
private?: boolean;
|
|
30
|
-
workspaces?: unknown;
|
|
31
|
-
devDependencies?: unknown;
|
|
32
|
-
dependencies?: unknown;
|
|
33
|
-
nx?: unknown;
|
|
34
|
-
repository?: unknown;
|
|
32
|
+
json: PackageJson;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
export interface RepositoryInfo {
|
|
@@ -46,6 +44,8 @@ export const workspaceDependencyFields = [
|
|
|
46
44
|
'optionalDependencies',
|
|
47
45
|
] as const;
|
|
48
46
|
|
|
47
|
+
export type WorkspaceDependencyField = (typeof workspaceDependencyFields)[number];
|
|
48
|
+
|
|
49
49
|
export function listPublicPackages(root: string): PackageInfo[] {
|
|
50
50
|
return getWorkspacePackages(root).filter((pkg) => !pkg.private && pkg.tags.includes('npm:public'));
|
|
51
51
|
}
|
|
@@ -54,26 +54,21 @@ export function listReleasePackages(
|
|
|
54
54
|
root: string,
|
|
55
55
|
rootPackage = readPackageJson(join(root, 'package.json')),
|
|
56
56
|
): PackageInfo[] {
|
|
57
|
-
|
|
58
|
-
return [];
|
|
59
|
-
}
|
|
60
|
-
const rootRepository = repositoryInfo(rootPackage.json);
|
|
57
|
+
const rootRepository = rootPackage ? repositoryInfo(rootPackage.json) : null;
|
|
61
58
|
if (!rootRepository) {
|
|
62
59
|
return [];
|
|
63
60
|
}
|
|
64
|
-
return
|
|
65
|
-
(pkg) => !pkg.private && pkg.tags.includes('npm:public') && isOwnedPackage(rootRepository, pkg),
|
|
66
|
-
);
|
|
61
|
+
return listPublicPackages(root).filter((pkg) => isOwnedPackage(rootRepository, pkg));
|
|
67
62
|
}
|
|
68
63
|
|
|
69
64
|
export function rootRepositoryInfo(root: string): RepositoryInfo | null {
|
|
70
|
-
const rootPackage =
|
|
71
|
-
return rootPackage ? repositoryInfo(rootPackage) : null;
|
|
65
|
+
const rootPackage = readPackageJson(join(root, 'package.json'));
|
|
66
|
+
return rootPackage ? repositoryInfo(rootPackage.json) : null;
|
|
72
67
|
}
|
|
73
68
|
|
|
74
69
|
export function rootPackageName(root: string): string | null {
|
|
75
70
|
const rootPackage = readPackageJsonObject(join(root, 'package.json'));
|
|
76
|
-
return rootPackage
|
|
71
|
+
return rootPackage?.name ?? null;
|
|
77
72
|
}
|
|
78
73
|
|
|
79
74
|
export function isSmoothBricksCodebase(root: string): boolean {
|
|
@@ -90,37 +85,23 @@ export function isOwnedPackage(rootRepository: RepositoryInfo, pkg: PackageInfo)
|
|
|
90
85
|
}
|
|
91
86
|
|
|
92
87
|
export function getWorkspacePackages(root: string): PackageInfo[] {
|
|
93
|
-
|
|
94
|
-
if (!rootPackage) {
|
|
95
|
-
throw new Error('package.json not found or invalid');
|
|
96
|
-
}
|
|
97
|
-
return getWorkspacePackagesForPatterns(root, getWorkspacePatternsFromPackageJson(rootPackage.json));
|
|
88
|
+
return getWorkspacePackagesForPatterns(root, getWorkspacePatterns(root));
|
|
98
89
|
}
|
|
99
90
|
|
|
100
91
|
export function getWorkspacePackageManifests(root: string): WorkspacePackageManifest[] {
|
|
101
|
-
|
|
102
|
-
if (!rootPackage) {
|
|
103
|
-
throw new Error('package.json not found or invalid');
|
|
104
|
-
}
|
|
105
|
-
return getWorkspacePackageManifestsForPatterns(root, getWorkspacePatternsFromPackageJson(rootPackage));
|
|
92
|
+
return getWorkspacePackageManifestsForPatterns(root, getWorkspacePatterns(root));
|
|
106
93
|
}
|
|
107
94
|
|
|
108
95
|
function getWorkspacePackagesForPatterns(root: string, workspacePatterns: string[]): PackageInfo[] {
|
|
109
96
|
const packages: PackageInfo[] = [];
|
|
110
|
-
for (const
|
|
111
|
-
const pkg = readPackageJson(
|
|
112
|
-
if (!pkg
|
|
97
|
+
for (const path of listWorkspacePackageJsonPaths(root, workspacePatterns)) {
|
|
98
|
+
const pkg = readPackageJson(path);
|
|
99
|
+
if (!pkg) {
|
|
113
100
|
continue;
|
|
114
101
|
}
|
|
115
102
|
packages.push({
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
version: pkg.version,
|
|
119
|
-
private: pkg.private,
|
|
120
|
-
tags: pkg.tags,
|
|
121
|
-
path: relative(root, dirname(pkgPath)),
|
|
122
|
-
packageJsonPath: pkg.packageJsonPath,
|
|
123
|
-
json: pkg.json,
|
|
103
|
+
...pkg,
|
|
104
|
+
path: relative(root, dirname(path)),
|
|
124
105
|
});
|
|
125
106
|
}
|
|
126
107
|
return packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -130,30 +111,42 @@ function getWorkspacePackageManifestsForPatterns(
|
|
|
130
111
|
root: string,
|
|
131
112
|
workspacePatterns: string[],
|
|
132
113
|
): WorkspacePackageManifest[] {
|
|
133
|
-
const
|
|
134
|
-
for (const
|
|
135
|
-
const pkg = readWorkspacePackageManifest(
|
|
136
|
-
if (pkg) {
|
|
137
|
-
|
|
114
|
+
const packages: WorkspacePackageManifest[] = [];
|
|
115
|
+
for (const path of listWorkspacePackageJsonPaths(root, workspacePatterns)) {
|
|
116
|
+
const pkg = readWorkspacePackageManifest(path);
|
|
117
|
+
if (!pkg) {
|
|
118
|
+
continue;
|
|
138
119
|
}
|
|
120
|
+
packages.push({
|
|
121
|
+
...pkg,
|
|
122
|
+
path: relative(root, dirname(path)),
|
|
123
|
+
});
|
|
139
124
|
}
|
|
140
|
-
return
|
|
125
|
+
return packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
141
126
|
}
|
|
142
127
|
|
|
143
128
|
function listWorkspacePackageJsonPaths(root: string, workspacePatterns: string[]): string[] {
|
|
144
129
|
const paths: string[] = [];
|
|
145
130
|
for (const pattern of workspacePatterns) {
|
|
146
131
|
if (!pattern.endsWith('/*')) {
|
|
147
|
-
paths.push(join(root, pattern, 'package.json'));
|
|
148
132
|
continue;
|
|
149
133
|
}
|
|
150
|
-
|
|
151
134
|
const parent = join(root, pattern.slice(0, -2));
|
|
152
|
-
|
|
135
|
+
let entries: string[];
|
|
136
|
+
try {
|
|
137
|
+
entries = readdirSync(parent);
|
|
138
|
+
} catch {
|
|
153
139
|
continue;
|
|
154
140
|
}
|
|
155
|
-
for (const entry of
|
|
156
|
-
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
const packageJsonPath = join(parent, entry, 'package.json');
|
|
143
|
+
try {
|
|
144
|
+
if (statSync(packageJsonPath).isFile()) {
|
|
145
|
+
paths.push(packageJsonPath);
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// skip missing package.json
|
|
149
|
+
}
|
|
157
150
|
}
|
|
158
151
|
}
|
|
159
152
|
return paths;
|
|
@@ -162,7 +155,7 @@ function listWorkspacePackageJsonPaths(root: string, workspacePatterns: string[]
|
|
|
162
155
|
export function listPackageJsonRecords(root: string): PackageInfo[] {
|
|
163
156
|
const rootPackage = readPackageJson(join(root, 'package.json'));
|
|
164
157
|
if (!rootPackage) {
|
|
165
|
-
|
|
158
|
+
return getWorkspacePackages(root);
|
|
166
159
|
}
|
|
167
160
|
return [
|
|
168
161
|
{ ...rootPackage, path: '.' },
|
|
@@ -172,18 +165,16 @@ export function listPackageJsonRecords(root: string): PackageInfo[] {
|
|
|
172
165
|
|
|
173
166
|
export function getWorkspacePatterns(root: string): string[] {
|
|
174
167
|
const raw = readJson(join(root, 'package.json'));
|
|
175
|
-
|
|
168
|
+
const pkg = isPackageJson(raw) ? raw : null;
|
|
169
|
+
return pkg ? getWorkspacePatternsFromPackageJson(pkg) : ['packages/*'];
|
|
176
170
|
}
|
|
177
171
|
|
|
178
|
-
function getWorkspacePatternsFromPackageJson(pkg:
|
|
179
|
-
if (!hasOwn(pkg, 'workspaces')) {
|
|
180
|
-
return ['packages/*'];
|
|
181
|
-
}
|
|
172
|
+
function getWorkspacePatternsFromPackageJson(pkg: PackageJson): string[] {
|
|
182
173
|
const workspaces = pkg.workspaces;
|
|
183
174
|
if (Array.isArray(workspaces)) {
|
|
184
175
|
return workspaces.filter((entry): entry is string => typeof entry === 'string');
|
|
185
176
|
}
|
|
186
|
-
if (
|
|
177
|
+
if (workspaces && typeof workspaces === 'object' && Array.isArray(workspaces.packages)) {
|
|
187
178
|
return workspaces.packages.filter((entry): entry is string => typeof entry === 'string');
|
|
188
179
|
}
|
|
189
180
|
return ['packages/*'];
|
|
@@ -191,17 +182,15 @@ function getWorkspacePatternsFromPackageJson(pkg: Record<string, unknown>): stri
|
|
|
191
182
|
|
|
192
183
|
export function readPackageJson(path: string): PackageInfo | null {
|
|
193
184
|
const parsed = readPackageJsonObject(path);
|
|
194
|
-
if (!
|
|
185
|
+
if (!parsed?.name || !parsed.version) {
|
|
195
186
|
return null;
|
|
196
187
|
}
|
|
197
|
-
const privateValue = hasOwn(parsed, 'private') && typeof parsed.private === 'boolean' ? parsed.private : false;
|
|
198
|
-
const tags = getNxTags(parsed);
|
|
199
188
|
return {
|
|
200
189
|
name: parsed.name,
|
|
201
190
|
projectName: packageNxProjectName(parsed),
|
|
202
191
|
version: parsed.version,
|
|
203
|
-
private:
|
|
204
|
-
tags,
|
|
192
|
+
private: parsed.private === true,
|
|
193
|
+
tags: getNxTags(parsed),
|
|
205
194
|
path: dirname(path),
|
|
206
195
|
packageJsonPath: path,
|
|
207
196
|
json: parsed,
|
|
@@ -210,50 +199,48 @@ export function readPackageJson(path: string): PackageInfo | null {
|
|
|
210
199
|
|
|
211
200
|
export function readWorkspacePackageManifest(path: string): WorkspacePackageManifest | null {
|
|
212
201
|
const parsed = readPackageJsonObject(path);
|
|
213
|
-
if (!
|
|
202
|
+
if (!parsed?.name) {
|
|
214
203
|
return null;
|
|
215
204
|
}
|
|
216
|
-
const privateValue = hasOwn(parsed, 'private') && typeof parsed.private === 'boolean' ? parsed.private : false;
|
|
217
205
|
return {
|
|
218
206
|
name: parsed.name,
|
|
219
207
|
projectName: packageNxProjectName(parsed),
|
|
220
|
-
private:
|
|
208
|
+
private: parsed.private === true,
|
|
221
209
|
path: dirname(path),
|
|
222
210
|
packageJsonPath: path,
|
|
223
211
|
json: parsed,
|
|
224
212
|
};
|
|
225
213
|
}
|
|
226
214
|
|
|
227
|
-
function packageNxProjectName(pkg:
|
|
228
|
-
|
|
229
|
-
return (nx ? stringProperty(nx, 'name') : null) ?? stringProperty(pkg, 'name') ?? '';
|
|
215
|
+
function packageNxProjectName(pkg: PackageJson): string {
|
|
216
|
+
return pkg.nx?.name ?? pkg.name ?? '';
|
|
230
217
|
}
|
|
231
218
|
|
|
232
219
|
export function readPackageJsonObject(path: string): PackageJson | null {
|
|
233
|
-
|
|
234
|
-
return parsed;
|
|
220
|
+
return readJsonObject(path);
|
|
235
221
|
}
|
|
236
222
|
|
|
237
|
-
export function getNxTags(pkg:
|
|
238
|
-
|
|
223
|
+
export function getNxTags(pkg: PackageJson): string[] {
|
|
224
|
+
const tags = pkg.nx?.tags;
|
|
225
|
+
if (!Array.isArray(tags)) {
|
|
239
226
|
return [];
|
|
240
227
|
}
|
|
241
|
-
return
|
|
228
|
+
return tags.filter((tag): tag is string => typeof tag === 'string');
|
|
242
229
|
}
|
|
243
230
|
|
|
244
|
-
export function repositoryInfo(pkg:
|
|
231
|
+
export function repositoryInfo(pkg: PackageJson): RepositoryInfo | null {
|
|
245
232
|
const repository = pkg.repository;
|
|
246
233
|
if (typeof repository === 'string') {
|
|
247
234
|
return { type: 'git', url: repository };
|
|
248
235
|
}
|
|
249
|
-
if (!
|
|
236
|
+
if (!repository || typeof repository !== 'object') {
|
|
250
237
|
return null;
|
|
251
238
|
}
|
|
252
|
-
const url =
|
|
239
|
+
const url = repository.url;
|
|
253
240
|
if (!url) {
|
|
254
241
|
return null;
|
|
255
242
|
}
|
|
256
|
-
return { type:
|
|
243
|
+
return { type: repository.type ?? 'git', url };
|
|
257
244
|
}
|
|
258
245
|
|
|
259
246
|
export function sameRepositoryAfterNormalization(left: string, right: string): boolean {
|
|
@@ -261,25 +248,26 @@ export function sameRepositoryAfterNormalization(left: string, right: string): b
|
|
|
261
248
|
}
|
|
262
249
|
|
|
263
250
|
function normalizedRepositoryUrl(url: string): string {
|
|
264
|
-
const trimmed = url
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (https?.[1] && https[2]) {
|
|
272
|
-
return githubRepositoryKey(https[1], https[2]);
|
|
273
|
-
}
|
|
274
|
-
const ssh = /^git@github\.com:([^/]+)\/([^/]+)$/i.exec(trimmed);
|
|
275
|
-
if (ssh?.[1] && ssh[2]) {
|
|
276
|
-
return githubRepositoryKey(ssh[1], ssh[2]);
|
|
251
|
+
const trimmed = url.trim().replace(/\.git$/i, '');
|
|
252
|
+
const ssh = /^git@github\.com:(.+)$/i.exec(trimmed);
|
|
253
|
+
if (ssh?.[1]) {
|
|
254
|
+
const [owner, repo] = ssh[1].split('/');
|
|
255
|
+
if (owner && repo) {
|
|
256
|
+
return githubRepositoryKey(owner, repo);
|
|
257
|
+
}
|
|
277
258
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
259
|
+
try {
|
|
260
|
+
const parsed = new URL(trimmed.includes('://') ? trimmed : `https://${trimmed}`);
|
|
261
|
+
if (parsed.hostname.replace(/^www\./i, '').toLowerCase() === 'github.com') {
|
|
262
|
+
const [owner, repo] = parsed.pathname.replace(/^\//, '').split('/');
|
|
263
|
+
if (owner && repo) {
|
|
264
|
+
return githubRepositoryKey(owner, repo);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
// fall through
|
|
281
269
|
}
|
|
282
|
-
return trimmed;
|
|
270
|
+
return trimmed.toLowerCase();
|
|
283
271
|
}
|
|
284
272
|
|
|
285
273
|
function githubRepositoryKey(owner: string, repo: string): string {
|
|
@@ -17,14 +17,14 @@ describe('CI workflow definition', () => {
|
|
|
17
17
|
const steps = defineCiWorkflow({ deploy: true, pushBranches: ['main'] });
|
|
18
18
|
const rendered = renderCiWorkflowYaml({ deploy: true, pushBranches: ['main'] });
|
|
19
19
|
|
|
20
|
-
expect(steps.map((step) => [step.kind, step.number])).toContainEqual([CiWorkflowStepKind.Deploy,
|
|
20
|
+
expect(steps.map((step) => [step.kind, step.number])).toContainEqual([CiWorkflowStepKind.Deploy, 11]);
|
|
21
21
|
expect(rendered).toContain('- name: ๐ Deploy Staging');
|
|
22
22
|
expect(rendered).not.toContain('CLOUDFLARE_API_TOKEN');
|
|
23
23
|
expect(rendered).not.toContain('CLOUDFLARE_ACCOUNT_ID');
|
|
24
24
|
expect(rendered).toContain(
|
|
25
|
-
'smoo github-ci nx-deploy --configuration staging --mode affected --name "Deploy Staging" --step
|
|
25
|
+
'smoo github-ci nx-deploy --configuration staging --mode affected --name "Deploy Staging" --step 11',
|
|
26
26
|
);
|
|
27
|
-
expect(rendered).toContain("# Step
|
|
27
|
+
expect(rendered).toContain("# Step 12\n # Nx's database cache needs artifact files");
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
it('adds Cloudflare credentials for Wrangler-backed deploys', () => {
|
|
@@ -33,4 +33,15 @@ describe('CI workflow definition', () => {
|
|
|
33
33
|
expect(rendered).toContain('CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}');
|
|
34
34
|
expect(rendered).toContain('CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}');
|
|
35
35
|
});
|
|
36
|
+
|
|
37
|
+
it('uses the same architecture-scoped key to restore and save the Nx cache', async () => {
|
|
38
|
+
const rendered = renderCiWorkflowYaml({ deploy: false, pushBranches: ['main'] });
|
|
39
|
+
const packageRoot = join(import.meta.dir, '..', '..', '..');
|
|
40
|
+
const restoreAction = await readFile(join(packageRoot, '..', '..', '.github/actions/cache-nx/action.yml'), 'utf8');
|
|
41
|
+
const restoreKey = restoreAction.match(/^\s*key: (.+)$/m)?.[1];
|
|
42
|
+
const saveKey = rendered.match(/^\s*key: (.+)$/m)?.[1];
|
|
43
|
+
|
|
44
|
+
expect(restoreKey).toBe('${{ runner.os }}-${{ runner.arch }}-nx-db-v1-${{ github.sha }}');
|
|
45
|
+
expect(saveKey).toBe(restoreKey);
|
|
46
|
+
});
|
|
36
47
|
});
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
import { describe, expect, it } from 'bun:test';
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
LINUX_PLATFORM_TARGET_GLOBS,
|
|
8
|
+
MACOS_PLATFORM_TARGET_GLOBS,
|
|
9
|
+
PLATFORM_TARGET_GLOBS,
|
|
10
|
+
} from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
11
|
+
import { format } from 'prettier';
|
|
6
12
|
import {
|
|
7
13
|
definePublishWorkflow,
|
|
8
14
|
type PublishWorkflowBump,
|
|
@@ -15,13 +21,208 @@ import {
|
|
|
15
21
|
|
|
16
22
|
describe('publish workflow definition', () => {
|
|
17
23
|
it('renders the checked-in local publish workflow copy', async () => {
|
|
18
|
-
const rendered = renderPublishWorkflowYaml({
|
|
24
|
+
const rendered = renderPublishWorkflowYaml({
|
|
25
|
+
repoName: '@smoothbricks/codebase',
|
|
26
|
+
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
27
|
+
});
|
|
19
28
|
const packageRoot = join(import.meta.dir, '..', '..', '..');
|
|
20
29
|
await expect(readFile(join(packageRoot, '..', '..', '.github/workflows/publish.yml'), 'utf8')).resolves.toBe(
|
|
21
30
|
rendered,
|
|
22
31
|
);
|
|
23
32
|
});
|
|
24
33
|
|
|
34
|
+
it('renders workflow bytes that are stable under the repository Prettier config', async () => {
|
|
35
|
+
const rendered = renderPublishWorkflowYaml({
|
|
36
|
+
repoName: '@smoothbricks/codebase',
|
|
37
|
+
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await expect(
|
|
41
|
+
format(rendered, {
|
|
42
|
+
parser: 'yaml',
|
|
43
|
+
printWidth: 120,
|
|
44
|
+
proseWrap: 'always',
|
|
45
|
+
}),
|
|
46
|
+
).resolves.toBe(rendered);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('preserves the single Ubuntu job and renders no artifact transfer when no Apple targets exist', () => {
|
|
50
|
+
const current = renderPublishWorkflowYaml({ repoName: '@smoothbricks/codebase' });
|
|
51
|
+
const explicitlyEmpty = renderPublishWorkflowYaml({
|
|
52
|
+
repoName: '@smoothbricks/codebase',
|
|
53
|
+
platformTargetGlobs: [],
|
|
54
|
+
});
|
|
55
|
+
const linuxOnly = renderPublishWorkflowYaml({
|
|
56
|
+
repoName: '@smoothbricks/codebase',
|
|
57
|
+
platformTargetGlobs: LINUX_PLATFORM_TARGET_GLOBS,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(explicitlyEmpty).toBe(current);
|
|
61
|
+
for (const rendered of [current, linuxOnly]) {
|
|
62
|
+
const jobsYaml = rendered.slice(rendered.indexOf('jobs:\n') + 'jobs:\n'.length);
|
|
63
|
+
expect(jobsYaml).toContain(' publish:\n runs-on: ubuntu-latest');
|
|
64
|
+
expect(jobsYaml.match(/^ {2}[a-z][a-z-]+:\n/gm)).toHaveLength(1);
|
|
65
|
+
expect(rendered).not.toContain('linux-release-candidate:');
|
|
66
|
+
expect(rendered).not.toContain('macos-platform:');
|
|
67
|
+
expect(rendered).not.toContain('publish-on-linux:');
|
|
68
|
+
expect(rendered).not.toContain('--collect-outputs');
|
|
69
|
+
expect(rendered).not.toContain('Upload validated release state');
|
|
70
|
+
expect(rendered).not.toContain('Upload validated build outputs');
|
|
71
|
+
expect(rendered).not.toContain('Upload supplemental Linux outputs');
|
|
72
|
+
expect(rendered).not.toContain('Upload macOS platform outputs');
|
|
73
|
+
expect(rendered).not.toContain('actions/download-artifact');
|
|
74
|
+
expect(rendered).not.toContain('apply-outputs');
|
|
75
|
+
expect(rendered).not.toContain('git bundle');
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('keeps job-local deeplink step anchors exact for single-job and parallel platform workflows', () => {
|
|
80
|
+
const singleJob = renderPublishWorkflowYaml({ repoName: '@smoothbricks/codebase' });
|
|
81
|
+
const platform = renderPublishWorkflowYaml({
|
|
82
|
+
deploy: true,
|
|
83
|
+
repoName: '@smoothbricks/codebase',
|
|
84
|
+
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
85
|
+
});
|
|
86
|
+
const linuxCandidate = platform.slice(
|
|
87
|
+
platform.indexOf(' linux-release-candidate:'),
|
|
88
|
+
platform.indexOf(' macos-platform:'),
|
|
89
|
+
);
|
|
90
|
+
const macosPlatform = platform.slice(
|
|
91
|
+
platform.indexOf(' macos-platform:'),
|
|
92
|
+
platform.indexOf(' publish-on-linux:'),
|
|
93
|
+
);
|
|
94
|
+
const finalJob = platform.slice(platform.indexOf(' publish-on-linux:'));
|
|
95
|
+
|
|
96
|
+
expect(stepAnchorNumbers(singleJob)).toEqual(Array.from({ length: 15 }, (_, index) => index + 1));
|
|
97
|
+
expect(stepAnchorNumbers(linuxCandidate)).toEqual(Array.from({ length: 19 }, (_, index) => index + 1));
|
|
98
|
+
expect(stepAnchorNumbers(macosPlatform)).toEqual(Array.from({ length: 7 }, (_, index) => index + 1));
|
|
99
|
+
expect(stepAnchorNumbers(finalJob)).toEqual(Array.from({ length: 14 }, (_, index) => index + 1));
|
|
100
|
+
expect(singleJob).toContain('# Step 14\n - name: ๐ฆ Publish release (${{ steps.version.outputs.mode }})');
|
|
101
|
+
expect(singleJob).toContain('# Step 15\n - name: ๐งน Cleanup and cache Nix/devenv');
|
|
102
|
+
expect(linuxCandidate).toContain('# Step 7\n - name: ๐ Capture candidate release SHA');
|
|
103
|
+
expect(linuxCandidate).toContain(
|
|
104
|
+
'# Step 8\n - name: โ
Check managed monorepo files (${{ steps.version.outputs.mode }})',
|
|
105
|
+
);
|
|
106
|
+
expect(linuxCandidate).toContain('# Step 19\n - name: ๐งน Cleanup and cache Nix/devenv');
|
|
107
|
+
expect(macosPlatform).toContain('# Step 5\n - name: ๐ Build selected macOS and iOS release outputs');
|
|
108
|
+
expect(macosPlatform).toContain('# Step 7\n - name: ๐งน Cleanup and cache Nix/devenv');
|
|
109
|
+
expect(finalJob).toContain('# Step 7\n - name: ๐งฏ Repair pending releases');
|
|
110
|
+
expect(finalJob).toContain('# Step 8\n - name: โป๏ธ Restore validated release state');
|
|
111
|
+
expect(finalJob).toContain('# Step 9\n - name: ๐ฆ Apply verified Linux outputs');
|
|
112
|
+
expect(finalJob).toContain('# Step 10\n - name: ๐ Apply verified macOS outputs');
|
|
113
|
+
expect(finalJob).toContain(
|
|
114
|
+
'# Step 12\n - name: ๐ฆ Publish release (${{ needs.linux-release-candidate.outputs.mode }})',
|
|
115
|
+
);
|
|
116
|
+
expect(finalJob).toContain('# Step 13\n - name: ๐ Deploy production');
|
|
117
|
+
expect(finalJob).toContain('# Step 14\n - name: ๐งน Cleanup and cache Nix/devenv');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('renders parallel native producers and a dependent publish-only final job', () => {
|
|
121
|
+
const linuxOnly = renderPublishWorkflowYaml({
|
|
122
|
+
repoName: '@smoothbricks/codebase',
|
|
123
|
+
platformTargetGlobs: LINUX_PLATFORM_TARGET_GLOBS,
|
|
124
|
+
});
|
|
125
|
+
const native = renderPublishWorkflowYaml({
|
|
126
|
+
repoName: '@smoothbricks/codebase',
|
|
127
|
+
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
128
|
+
});
|
|
129
|
+
const linuxCandidate = native.slice(
|
|
130
|
+
native.indexOf(' linux-release-candidate:'),
|
|
131
|
+
native.indexOf(' macos-platform:'),
|
|
132
|
+
);
|
|
133
|
+
const macosPlatform = native.slice(native.indexOf(' macos-platform:'), native.indexOf(' publish-on-linux:'));
|
|
134
|
+
const finalJob = native.slice(native.indexOf(' publish-on-linux:'));
|
|
135
|
+
|
|
136
|
+
expect(linuxOnly).not.toContain('macos-platform:');
|
|
137
|
+
expect(linuxOnly).toContain(
|
|
138
|
+
`smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(',')}" --projects`,
|
|
139
|
+
);
|
|
140
|
+
expect(native).toContain(' linux-release-candidate:\n runs-on: ubuntu-latest');
|
|
141
|
+
expect(native).toContain(' macos-platform:\n runs-on: macos-latest');
|
|
142
|
+
expect(native).toContain(' publish-on-linux:\n needs: [linux-release-candidate, macos-platform]');
|
|
143
|
+
expect(linuxCandidate).not.toContain('needs:');
|
|
144
|
+
expect(macosPlatform).not.toContain('needs:');
|
|
145
|
+
expect(linuxCandidate).not.toContain('smoo release repair-pending');
|
|
146
|
+
expect(linuxCandidate).toContain('smoo release version');
|
|
147
|
+
expect(linuxCandidate).toContain('smoo github-ci nx-run-many --targets build --projects');
|
|
148
|
+
expect(linuxCandidate).toContain('smoo github-ci nx-run-many --targets lint --projects');
|
|
149
|
+
expect(linuxCandidate).toContain('smoo github-ci nx-run-many --targets test --projects');
|
|
150
|
+
expect(linuxCandidate).toContain(
|
|
151
|
+
`smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(',')}" --projects`,
|
|
152
|
+
);
|
|
153
|
+
expect(macosPlatform).not.toContain('smoo github-ci nx-run-many');
|
|
154
|
+
expect(foldedRunCommand(macosPlatform, '๐ Build selected macOS and iOS release outputs')).toBe(
|
|
155
|
+
`smoo release build-platform-outputs --bump "\${{ inputs.bump }}" --ref "\${{ github.sha }}" --targets "${MACOS_PLATFORM_TARGET_GLOBS.join(
|
|
156
|
+
',',
|
|
157
|
+
)}" --output "\${{ runner.temp }}/macos-platform-outputs"`,
|
|
158
|
+
);
|
|
159
|
+
expect(finalJob).not.toContain('smoo release version');
|
|
160
|
+
expect(finalJob).not.toContain('nx-run-many');
|
|
161
|
+
expect(foldedRunCommand(finalJob, '๐งฏ Repair pending releases')).toBe(
|
|
162
|
+
'smoo release repair-pending --ref "${{ github.sha }}" --platform-outputs ' +
|
|
163
|
+
'"${{ runner.temp }}/publish-artifacts/publish-macos-outputs-${{ github.run_id }}/repairs" ' +
|
|
164
|
+
'--dry-run "${{ inputs.dry_run }}"',
|
|
165
|
+
);
|
|
166
|
+
expect(native).toContain('uses: actions/upload-artifact@v7.0.1');
|
|
167
|
+
expect(native).toContain('uses: actions/download-artifact@v8.0.1');
|
|
168
|
+
expect(native).toContain('name: publish-release-state-${{ github.run_id }}');
|
|
169
|
+
expect(native).toContain('name: publish-release-outputs-${{ github.run_id }}');
|
|
170
|
+
expect(native).toContain('name: publish-linux-outputs-${{ github.run_id }}');
|
|
171
|
+
expect(native.match(/include-hidden-files: true/g)).toHaveLength(4);
|
|
172
|
+
expect(native).toContain('name: publish-macos-outputs-${{ github.run_id }}');
|
|
173
|
+
expect(native).toContain('git bundle create');
|
|
174
|
+
expect(native).toContain('git fetch "${{ runner.temp }}/publish-artifacts/publish-release-state-');
|
|
175
|
+
expect(finalJob).toContain(
|
|
176
|
+
'smoo github-ci apply-outputs --source-sha "${{ needs.linux-release-candidate.outputs.release-sha }}"',
|
|
177
|
+
);
|
|
178
|
+
expect(finalJob).toContain('smoo github-ci apply-outputs --source-sha "${{ github.sha }}"');
|
|
179
|
+
expect(foldedRunCommand(finalJob, '๐ฆ Apply verified Linux outputs')).toBe(
|
|
180
|
+
'smoo github-ci apply-outputs --source-sha "${{ needs.linux-release-candidate.outputs.release-sha }}" ' +
|
|
181
|
+
'"${{ runner.temp }}/publish-artifacts/publish-release-outputs-${{ github.run_id }}" ' +
|
|
182
|
+
'"${{ runner.temp }}/publish-artifacts/publish-linux-outputs-${{ github.run_id }}"',
|
|
183
|
+
);
|
|
184
|
+
expect(foldedRunCommand(finalJob, '๐ Apply verified macOS outputs')).toBe(
|
|
185
|
+
'smoo github-ci apply-outputs --source-sha "${{ github.sha }}" ' +
|
|
186
|
+
'"${{ runner.temp }}/publish-artifacts/publish-macos-outputs-${{ github.run_id }}/current"',
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('limits trusted publishing permission to final publish and preserves mode, deploy, and dry-run gates', () => {
|
|
191
|
+
const rendered = renderPublishWorkflowYaml({
|
|
192
|
+
deploy: true,
|
|
193
|
+
deployProvider: 'cloudflare',
|
|
194
|
+
repoName: '@smoothbricks/codebase',
|
|
195
|
+
platformTargetGlobs: PLATFORM_TARGET_GLOBS,
|
|
196
|
+
});
|
|
197
|
+
const linuxCandidate = rendered.slice(
|
|
198
|
+
rendered.indexOf(' linux-release-candidate:'),
|
|
199
|
+
rendered.indexOf(' macos-platform:'),
|
|
200
|
+
);
|
|
201
|
+
const macosPlatform = rendered.slice(
|
|
202
|
+
rendered.indexOf(' macos-platform:'),
|
|
203
|
+
rendered.indexOf(' publish-on-linux:'),
|
|
204
|
+
);
|
|
205
|
+
const finalJob = rendered.slice(rendered.indexOf(' publish-on-linux:'));
|
|
206
|
+
|
|
207
|
+
expect(linuxCandidate).toContain('id-token: none');
|
|
208
|
+
expect(macosPlatform).toContain('id-token: none');
|
|
209
|
+
expect(finalJob).toContain('id-token: write');
|
|
210
|
+
expect(rendered.match(/id-token: write/g)).toHaveLength(1);
|
|
211
|
+
expect(finalJob.indexOf('๐งฏ Repair pending releases')).toBeLessThan(
|
|
212
|
+
finalJob.indexOf('โป๏ธ Restore validated release state'),
|
|
213
|
+
);
|
|
214
|
+
expect(finalJob.indexOf('โป๏ธ Restore validated release state')).toBeGreaterThan(
|
|
215
|
+
finalJob.indexOf('๐งฑ Setup Nix/devenv'),
|
|
216
|
+
);
|
|
217
|
+
expect(rendered).not.toContain('GITHUB_SHA:');
|
|
218
|
+
expect(finalJob).toContain("needs.linux-release-candidate.outputs.mode != 'none'");
|
|
219
|
+
expect(finalJob).toContain("inputs.deploy_environment == 'production'");
|
|
220
|
+
expect(finalJob).toContain("inputs.dry_run != 'true'");
|
|
221
|
+
expect(finalJob).toContain('smoo release publish --bump "${{ inputs.bump }}" --dry-run "${{ inputs.dry_run }}"');
|
|
222
|
+
expect(finalJob).toContain('CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}');
|
|
223
|
+
expect(finalJob).toContain('CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}');
|
|
224
|
+
});
|
|
225
|
+
|
|
25
226
|
it('does not wire npm token secrets into repair or publish steps', () => {
|
|
26
227
|
const rendered = renderPublishWorkflowYaml({ repoName: '@smoothbricks/codebase' });
|
|
27
228
|
|
|
@@ -384,6 +585,26 @@ class WorkflowScenarioState {
|
|
|
384
585
|
}
|
|
385
586
|
}
|
|
386
587
|
|
|
588
|
+
function stepAnchorNumbers(yaml: string): number[] {
|
|
589
|
+
return [...yaml.matchAll(/^\s*# Step (\d+)/gm)].map((match) => Number(match[1]));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function foldedRunCommand(yaml: string, stepName: string): string {
|
|
593
|
+
const stepStart = yaml.indexOf(`- name: ${stepName}`);
|
|
594
|
+
expect(stepStart).toBeGreaterThanOrEqual(0);
|
|
595
|
+
const runMarker = ' run:\n';
|
|
596
|
+
const runStart = yaml.indexOf(runMarker, stepStart);
|
|
597
|
+
expect(runStart).toBeGreaterThan(stepStart);
|
|
598
|
+
const commandLines: string[] = [];
|
|
599
|
+
for (const line of yaml.slice(runStart + runMarker.length).split('\n')) {
|
|
600
|
+
if (!line.startsWith(' ')) {
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
commandLines.push(line.trim());
|
|
604
|
+
}
|
|
605
|
+
return commandLines.join(' ');
|
|
606
|
+
}
|
|
607
|
+
|
|
387
608
|
function packageNameFromTag(tag: string): string {
|
|
388
609
|
const versionSeparator = tag.lastIndexOf('@');
|
|
389
610
|
if (versionSeparator <= 0) {
|