@smoothbricks/cli 0.3.3 โ 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +14 -0
- package/dist/github-ci/index.d.ts +12 -0
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +72 -4
- package/dist/monorepo/ci-workflow.d.ts +26 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -0
- package/dist/monorepo/ci-workflow.js +201 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +97 -5
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +4 -2
- package/dist/monorepo/publish-workflow.d.ts +7 -1
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +59 -15
- package/dist/monorepo/runtime.d.ts +3 -0
- package/dist/monorepo/runtime.d.ts.map +1 -1
- package/dist/monorepo/runtime.js +66 -2
- package/managed/raw/git-format-staged.yml +4 -0
- package/package.json +3 -3
- package/src/cli.ts +35 -5
- package/src/github-ci/index.ts +94 -8
- package/src/monorepo/__tests__/ci-workflow.test.ts +34 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +65 -2
- package/src/monorepo/ci-workflow.ts +229 -0
- package/src/monorepo/managed-files.ts +115 -5
- package/src/monorepo/package-policy.test.ts +24 -9
- package/src/monorepo/package-policy.ts +4 -2
- package/src/monorepo/publish-workflow.ts +73 -16
- package/src/monorepo/runtime.test.ts +30 -0
- package/src/monorepo/runtime.ts +94 -2
- package/src/release/__tests__/fixture-repo.test.ts +14 -13
- package/src/release/__tests__/helpers/fixture-repo.ts +85 -12
- package/managed/templates/github/workflows/ci.yml +0 -102
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { isSmoothBricksCodebasePackageName } from '../lib/cli-package.js';
|
|
2
2
|
|
|
3
3
|
export type PublishWorkflowBump = 'auto' | 'patch' | 'minor' | 'major' | 'prerelease';
|
|
4
|
-
export type PublishWorkflowCondition = 'version-mode-not-none' | 'failure' | 'always';
|
|
4
|
+
export type PublishWorkflowCondition = 'version-mode-not-none' | 'deploy-production' | 'failure' | 'always';
|
|
5
5
|
export type PublishWorkflowNxTarget = 'build' | 'lint' | 'test';
|
|
6
|
+
export type PublishWorkflowDeployEnvironment = 'none' | 'production';
|
|
6
7
|
|
|
7
8
|
export enum PublishWorkflowStepKind {
|
|
8
9
|
Checkout = 'checkout',
|
|
@@ -18,6 +19,7 @@ export enum PublishWorkflowStepKind {
|
|
|
18
19
|
UploadTraceDbs = 'upload-trace-dbs',
|
|
19
20
|
ValidateMonorepoConfig = 'validate-monorepo-config',
|
|
20
21
|
PublishRelease = 'publish-release',
|
|
22
|
+
DeployProduction = 'deploy-production',
|
|
21
23
|
SaveNixDevenv = 'save-nix-devenv',
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -35,11 +37,14 @@ export interface PublishWorkflowDefinition {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface PublishWorkflowDefinitionOptions {
|
|
40
|
+
deploy?: boolean;
|
|
41
|
+
deployProvider?: 'cloudflare';
|
|
38
42
|
repoName?: string;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export interface PublishWorkflowInputs {
|
|
42
46
|
bump: PublishWorkflowBump;
|
|
47
|
+
deployEnvironment: PublishWorkflowDeployEnvironment;
|
|
43
48
|
dryRun: boolean;
|
|
44
49
|
}
|
|
45
50
|
|
|
@@ -65,6 +70,7 @@ export interface PublishWorkflowCallbacks {
|
|
|
65
70
|
uploadTraceDbs(): Promise<void>;
|
|
66
71
|
validateMonorepoConfig(): Promise<void>;
|
|
67
72
|
publishRelease(input: { bump: PublishWorkflowBump; dryRun: boolean }): Promise<void>;
|
|
73
|
+
deployProduction(): Promise<void>;
|
|
68
74
|
saveNixDevenv(input: PublishWorkflowSetupOutputs): Promise<void>;
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -90,6 +96,19 @@ export function definePublishWorkflow(options: PublishWorkflowDefinitionOptions
|
|
|
90
96
|
if (isSmoothBricksCodebasePackageName(options.repoName)) {
|
|
91
97
|
setupSteps.push({ kind: PublishWorkflowStepKind.BuildSelfHostedCli, name: '๐๏ธ Build self-hosted smoo' });
|
|
92
98
|
}
|
|
99
|
+
const releaseSteps: PublishWorkflowStepInput[] = [
|
|
100
|
+
{
|
|
101
|
+
kind: PublishWorkflowStepKind.PublishRelease,
|
|
102
|
+
name: `๐ฆ Publish release (${versionMode})`,
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
if (options.deploy === true) {
|
|
106
|
+
releaseSteps.push({
|
|
107
|
+
kind: PublishWorkflowStepKind.DeployProduction,
|
|
108
|
+
name: '๐ Deploy production',
|
|
109
|
+
condition: 'deploy-production',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
93
112
|
return {
|
|
94
113
|
steps: numberWorkflowSteps([
|
|
95
114
|
...setupSteps,
|
|
@@ -127,10 +146,7 @@ export function definePublishWorkflow(options: PublishWorkflowDefinitionOptions
|
|
|
127
146
|
name: `โ
Validate monorepo config (${versionMode})`,
|
|
128
147
|
condition: 'version-mode-not-none',
|
|
129
148
|
},
|
|
130
|
-
|
|
131
|
-
kind: PublishWorkflowStepKind.PublishRelease,
|
|
132
|
-
name: `๐ฆ Publish release (${versionMode})`,
|
|
133
|
-
},
|
|
149
|
+
...releaseSteps,
|
|
134
150
|
{
|
|
135
151
|
kind: PublishWorkflowStepKind.SaveNixDevenv,
|
|
136
152
|
name: '๐งน Cleanup and cache Nix/devenv',
|
|
@@ -153,7 +169,7 @@ export async function runPublishWorkflow(
|
|
|
153
169
|
let failed = false;
|
|
154
170
|
let failure: unknown;
|
|
155
171
|
for (const step of workflow.steps) {
|
|
156
|
-
if (!shouldRunStep(step, version, failed)) {
|
|
172
|
+
if (!shouldRunStep(step, version, failed, context.inputs)) {
|
|
157
173
|
continue;
|
|
158
174
|
}
|
|
159
175
|
try {
|
|
@@ -201,6 +217,9 @@ export async function runPublishWorkflow(
|
|
|
201
217
|
dryRun: context.inputs.dryRun,
|
|
202
218
|
});
|
|
203
219
|
break;
|
|
220
|
+
case PublishWorkflowStepKind.DeployProduction:
|
|
221
|
+
await context.callbacks.deployProduction();
|
|
222
|
+
break;
|
|
204
223
|
case PublishWorkflowStepKind.SaveNixDevenv:
|
|
205
224
|
await context.callbacks.saveNixDevenv(setupOutputs);
|
|
206
225
|
break;
|
|
@@ -216,10 +235,18 @@ export async function runPublishWorkflow(
|
|
|
216
235
|
return { version, failed };
|
|
217
236
|
}
|
|
218
237
|
|
|
219
|
-
function shouldRunStep(
|
|
238
|
+
function shouldRunStep(
|
|
239
|
+
step: PublishWorkflowStep,
|
|
240
|
+
version: PublishWorkflowVersionOutputs,
|
|
241
|
+
failed: boolean,
|
|
242
|
+
inputs: PublishWorkflowInputs,
|
|
243
|
+
): boolean {
|
|
220
244
|
if (step.condition === 'version-mode-not-none') {
|
|
221
245
|
return version.mode !== 'none';
|
|
222
246
|
}
|
|
247
|
+
if (step.condition === 'deploy-production') {
|
|
248
|
+
return version.mode !== 'none' && inputs.deployEnvironment === 'production' && !inputs.dryRun;
|
|
249
|
+
}
|
|
223
250
|
if (step.condition === 'failure') {
|
|
224
251
|
return failed;
|
|
225
252
|
}
|
|
@@ -228,10 +255,19 @@ function shouldRunStep(step: PublishWorkflowStep, version: PublishWorkflowVersio
|
|
|
228
255
|
|
|
229
256
|
export function renderPublishWorkflowYaml(options: PublishWorkflowDefinitionOptions = {}): string {
|
|
230
257
|
const steps = definePublishWorkflow(options).steps;
|
|
231
|
-
return `${renderPublishWorkflowHeader()}${renderPublishWorkflowSteps(steps)}`;
|
|
258
|
+
return `${renderPublishWorkflowHeader(options)}${renderPublishWorkflowSteps(steps, options)}`;
|
|
232
259
|
}
|
|
233
260
|
|
|
234
|
-
function renderPublishWorkflowHeader(): string {
|
|
261
|
+
function renderPublishWorkflowHeader(options: PublishWorkflowDefinitionOptions): string {
|
|
262
|
+
const deployInput =
|
|
263
|
+
options.deploy === true
|
|
264
|
+
? `
|
|
265
|
+
deploy_environment:
|
|
266
|
+
type: choice
|
|
267
|
+
description: Deploy live systems after a successful publish.
|
|
268
|
+
options: [none, production]
|
|
269
|
+
default: none`
|
|
270
|
+
: '';
|
|
235
271
|
return `name: Publish
|
|
236
272
|
|
|
237
273
|
on:
|
|
@@ -247,7 +283,7 @@ on:
|
|
|
247
283
|
dry_run:
|
|
248
284
|
type: boolean
|
|
249
285
|
description: Run release commands without writing versions, tags, publishes, or GitHub Releases.
|
|
250
|
-
default: false
|
|
286
|
+
default: false${deployInput}
|
|
251
287
|
|
|
252
288
|
permissions:
|
|
253
289
|
contents: write
|
|
@@ -271,12 +307,12 @@ jobs:
|
|
|
271
307
|
`;
|
|
272
308
|
}
|
|
273
309
|
|
|
274
|
-
function renderPublishWorkflowSteps(steps: PublishWorkflowStep[]): string {
|
|
310
|
+
function renderPublishWorkflowSteps(steps: PublishWorkflowStep[], options: PublishWorkflowDefinitionOptions): string {
|
|
275
311
|
const lines: string[] = [];
|
|
276
312
|
for (const step of steps) {
|
|
277
313
|
lines.push(...sectionLinesBefore(step));
|
|
278
314
|
lines.push(...commentLinesForStep(step));
|
|
279
|
-
lines.push(...yamlLinesForStep(step));
|
|
315
|
+
lines.push(...yamlLinesForStep(step, options));
|
|
280
316
|
lines.push('');
|
|
281
317
|
}
|
|
282
318
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
@@ -319,7 +355,7 @@ function commentLinesForStep(step: PublishWorkflowStep): string[] {
|
|
|
319
355
|
return [` # Step ${step.number}`];
|
|
320
356
|
}
|
|
321
357
|
|
|
322
|
-
function yamlLinesForStep(step: PublishWorkflowStep): string[] {
|
|
358
|
+
function yamlLinesForStep(step: PublishWorkflowStep, options: PublishWorkflowDefinitionOptions): string[] {
|
|
323
359
|
switch (step.kind) {
|
|
324
360
|
case PublishWorkflowStepKind.Checkout:
|
|
325
361
|
return [
|
|
@@ -396,6 +432,8 @@ function yamlLinesForStep(step: PublishWorkflowStep): string[] {
|
|
|
396
432
|
' # Missing package names are bootstrapped locally before trust setup.',
|
|
397
433
|
` run: smoo release publish --bump "${githubExpression('inputs.bump')}" --dry-run "${githubExpression('inputs.dry_run')}"`,
|
|
398
434
|
];
|
|
435
|
+
case PublishWorkflowStepKind.DeployProduction:
|
|
436
|
+
return deployProductionStep(step, options);
|
|
399
437
|
case PublishWorkflowStepKind.SaveNixDevenv:
|
|
400
438
|
return [
|
|
401
439
|
` - name: ${step.name}`,
|
|
@@ -408,14 +446,33 @@ function yamlLinesForStep(step: PublishWorkflowStep): string[] {
|
|
|
408
446
|
}
|
|
409
447
|
}
|
|
410
448
|
|
|
411
|
-
function
|
|
449
|
+
function deployProductionStep(step: PublishWorkflowStep, options: PublishWorkflowDefinitionOptions): string[] {
|
|
412
450
|
return [
|
|
413
451
|
` - name: ${step.name}`,
|
|
414
|
-
|
|
415
|
-
|
|
452
|
+
' if:',
|
|
453
|
+
" ${{ steps.version.outputs.mode != 'none' && inputs.deploy_environment == 'production' && inputs.dry_run !=",
|
|
454
|
+
" 'true' }}",
|
|
455
|
+
...deployEnvLines(options),
|
|
456
|
+
' run: smoo github-ci nx-deploy --configuration production --mode run-many --verify --name "Deploy Production"',
|
|
416
457
|
];
|
|
417
458
|
}
|
|
418
459
|
|
|
460
|
+
function deployEnvLines(options: PublishWorkflowDefinitionOptions): string[] {
|
|
461
|
+
if (options.deployProvider !== 'cloudflare') {
|
|
462
|
+
return [];
|
|
463
|
+
}
|
|
464
|
+
return [
|
|
465
|
+
' env:',
|
|
466
|
+
' CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}',
|
|
467
|
+
' CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}',
|
|
468
|
+
];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function conditionalRunStep(step: PublishWorkflowStep, run: string): string[] {
|
|
472
|
+
const condition = "steps.version.outputs.mode != 'none'";
|
|
473
|
+
return [` - name: ${step.name}`, ` if: ${githubExpression(condition)}`, ` run: ${run}`];
|
|
474
|
+
}
|
|
475
|
+
|
|
419
476
|
function githubExpression(expression: string): string {
|
|
420
477
|
return ['$', '{{ ', expression, ' }}'].join('');
|
|
421
478
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { runtimeTypesRangeForPublishedVersions } from './runtime.js';
|
|
3
|
+
|
|
4
|
+
describe('runtimeTypesRangeForPublishedVersions', () => {
|
|
5
|
+
test('uses the installed Node major when @types/node has published it', () => {
|
|
6
|
+
expect(
|
|
7
|
+
runtimeTypesRangeForPublishedVersions('@types/node', '24.12.0', 'major', ['24.0.0', '24.12.4', '25.9.1']),
|
|
8
|
+
).toBe('~24.0.0');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('falls back to latest published @types/node when the Node major is unpublished', () => {
|
|
12
|
+
expect(
|
|
13
|
+
runtimeTypesRangeForPublishedVersions('@types/node', '26.0.0', 'major', ['24.12.4', '25.9.0', '25.9.1']),
|
|
14
|
+
).toBe('~25.9.1');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('uses the installed Bun version when @types/bun has published it', () => {
|
|
18
|
+
expect(runtimeTypesRangeForPublishedVersions('@types/bun', '1.3.14', 'exact', ['1.3.13', '1.3.14'])).toBe('1.3.14');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('falls back to latest published @types/bun when the Bun version is unpublished', () => {
|
|
22
|
+
expect(runtimeTypesRangeForPublishedVersions('@types/bun', '1.3.15', 'exact', ['1.3.13', '1.3.14'])).toBe('1.3.14');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('ignores non-stable version strings when choosing the fallback', () => {
|
|
26
|
+
expect(runtimeTypesRangeForPublishedVersions('@types/node', '26.0.0', 'major', ['25.9.1', '26.0.0-beta.1'])).toBe(
|
|
27
|
+
'~25.9.1',
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/monorepo/runtime.ts
CHANGED
|
@@ -23,8 +23,18 @@ export async function syncRootRuntimeVersions(root: string): Promise<void> {
|
|
|
23
23
|
changed = setStringProperty(engines, 'node', `>=${nodeMajor}.0.0`) || changed;
|
|
24
24
|
changed = setStringProperty(packageJson, 'packageManager', `bun@${bunVersion}`) || changed;
|
|
25
25
|
const devDependencies = getOrCreateRecord(packageJson, 'devDependencies');
|
|
26
|
-
changed =
|
|
27
|
-
|
|
26
|
+
changed =
|
|
27
|
+
setStringProperty(
|
|
28
|
+
devDependencies,
|
|
29
|
+
'@types/node',
|
|
30
|
+
await runtimeTypesRange(root, '@types/node', nodeVersion, 'major'),
|
|
31
|
+
) || changed;
|
|
32
|
+
changed =
|
|
33
|
+
setStringProperty(
|
|
34
|
+
devDependencies,
|
|
35
|
+
'@types/bun',
|
|
36
|
+
await runtimeTypesRange(root, '@types/bun', bunVersion, 'exact'),
|
|
37
|
+
) || changed;
|
|
28
38
|
|
|
29
39
|
if (changed) {
|
|
30
40
|
writeJsonObject(packageJsonPath, packageJson);
|
|
@@ -32,6 +42,88 @@ export async function syncRootRuntimeVersions(root: string): Promise<void> {
|
|
|
32
42
|
}
|
|
33
43
|
}
|
|
34
44
|
|
|
45
|
+
type RuntimeTypesPinMode = 'major' | 'exact';
|
|
46
|
+
|
|
47
|
+
async function runtimeTypesRange(
|
|
48
|
+
root: string,
|
|
49
|
+
packageName: string,
|
|
50
|
+
runtimeVersion: string,
|
|
51
|
+
pinMode: RuntimeTypesPinMode,
|
|
52
|
+
): Promise<string> {
|
|
53
|
+
const versionsText = await $`bun pm view ${packageName} versions --json`.cwd(root).text();
|
|
54
|
+
const versions = JSON.parse(versionsText) as unknown;
|
|
55
|
+
if (!Array.isArray(versions) || !versions.every((version) => typeof version === 'string')) {
|
|
56
|
+
throw new Error(`Unable to read published ${packageName} versions`);
|
|
57
|
+
}
|
|
58
|
+
return runtimeTypesRangeForPublishedVersions(packageName, runtimeVersion, pinMode, versions);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function runtimeTypesRangeForPublishedVersions(
|
|
62
|
+
packageName: string,
|
|
63
|
+
runtimeVersion: string,
|
|
64
|
+
pinMode: RuntimeTypesPinMode,
|
|
65
|
+
versions: readonly string[],
|
|
66
|
+
): string {
|
|
67
|
+
const parsedRuntimeVersion = parseVersion(runtimeVersion);
|
|
68
|
+
if (!parsedRuntimeVersion) {
|
|
69
|
+
throw new Error(`Unable to parse runtime version ${runtimeVersion}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (pinMode === 'major') {
|
|
73
|
+
const runtimeMajor = parsedRuntimeVersion[0].toString();
|
|
74
|
+
if (latestVersion(versions.filter((version) => versionMajor(version) === runtimeMajor))) {
|
|
75
|
+
return `~${runtimeMajor}.0.0`;
|
|
76
|
+
}
|
|
77
|
+
} else if (versions.includes(runtimeVersion)) {
|
|
78
|
+
return runtimeVersion;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const latest = latestVersion(versions);
|
|
82
|
+
if (!latest) {
|
|
83
|
+
throw new Error(`Unable to find any published ${packageName} versions`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.warn(`${packageName} has no published types for runtime ${runtimeVersion}; using ${latest}`);
|
|
87
|
+
return pinMode === 'major' ? `~${latest}` : latest;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function latestVersion(versions: readonly string[]): string | null {
|
|
91
|
+
let latest: string | null = null;
|
|
92
|
+
for (const version of versions) {
|
|
93
|
+
if (!parseVersion(version)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!latest || compareVersions(version, latest) > 0) {
|
|
97
|
+
latest = version;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return latest;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compareVersions(left: string, right: string): number {
|
|
104
|
+
const leftParts = parseVersion(left);
|
|
105
|
+
const rightParts = parseVersion(right);
|
|
106
|
+
if (!leftParts || !rightParts) {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
for (let index = 0; index < leftParts.length; index++) {
|
|
110
|
+
const diff = leftParts[index] - rightParts[index];
|
|
111
|
+
if (diff !== 0) {
|
|
112
|
+
return diff;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function versionMajor(version: string): string | null {
|
|
119
|
+
return parseVersion(version)?.[0].toString() ?? null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseVersion(version: string): [number, number, number] | null {
|
|
123
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
124
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
35
127
|
function runtimeCommand(root: string, name: 'bun' | 'node'): string {
|
|
36
128
|
const candidates = [
|
|
37
129
|
join(root, 'tooling', 'direnv', '.devenv', 'profile', 'bin', name),
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { $ } from 'bun';
|
|
5
4
|
import { collectOwnedReleaseTagRecords, pendingReleaseTargets, type ReleasePackageInfo, releaseTag } from '../core.js';
|
|
6
5
|
import { completeReleaseAtHead, type ReleaseRepairShell, repairPendingTargets } from '../orchestration.js';
|
|
7
6
|
import {
|
|
@@ -9,7 +8,9 @@ import {
|
|
|
9
8
|
gitIsAncestor,
|
|
10
9
|
gitOutput,
|
|
11
10
|
gitReleaseTagsByCreatorDate,
|
|
11
|
+
gitSucceeds,
|
|
12
12
|
packageVersionAtRef,
|
|
13
|
+
runFixtureNx,
|
|
13
14
|
tag,
|
|
14
15
|
withFixtureRepo,
|
|
15
16
|
writeBuildablePackage,
|
|
@@ -78,7 +79,7 @@ describe('release planning with fixture git repositories', () => {
|
|
|
78
79
|
await git(root, ['init', '--bare', 'remote.git']);
|
|
79
80
|
await git(root, ['remote', 'add', 'origin', join(root, 'remote.git')]);
|
|
80
81
|
await git(root, ['push', 'origin', 'main', '--tags']);
|
|
81
|
-
await
|
|
82
|
+
await git(root, ['clone', '--branch', 'main', join(root, 'remote.git'), 'checkout']);
|
|
82
83
|
const checkout = join(root, 'checkout');
|
|
83
84
|
await git(checkout, ['fetch', '--tags', 'origin', 'main']);
|
|
84
85
|
const head = await gitOutput(checkout, ['rev-parse', 'HEAD']);
|
|
@@ -105,7 +106,7 @@ describe('release planning with fixture git repositories', () => {
|
|
|
105
106
|
await writeBuildablePackage(root, '@scope/a', 'packages/a');
|
|
106
107
|
await writeBuildablePackage(root, '@scope/b', 'packages/b');
|
|
107
108
|
|
|
108
|
-
await
|
|
109
|
+
await runFixtureNx(root, ['run-many', '-t', 'build', '--projects=a,b']);
|
|
109
110
|
|
|
110
111
|
await expect(readFile(join(root, 'packages/a/dist/index.js'), 'utf8')).resolves.toBe('{}\n');
|
|
111
112
|
await expect(readFile(join(root, 'packages/b/dist/index.js'), 'utf8')).resolves.toBe('{}\n');
|
|
@@ -143,7 +144,7 @@ describe('release planning with fixture git repositories', () => {
|
|
|
143
144
|
await git(author, ['init', '--bare', 'remote.git']);
|
|
144
145
|
await git(author, ['remote', 'add', 'origin', join(author, 'remote.git')]);
|
|
145
146
|
await git(author, ['push', 'origin', 'main', '--tags']);
|
|
146
|
-
await
|
|
147
|
+
await git(author, ['clone', '--branch', 'main', join(author, 'remote.git'), 'runner']);
|
|
147
148
|
const runner = join(author, 'runner');
|
|
148
149
|
await git(runner, ['config', 'user.name', 'Test User']);
|
|
149
150
|
await git(runner, ['config', 'user.email', 'test@example.com']);
|
|
@@ -197,7 +198,7 @@ describe('release planning with fixture git repositories', () => {
|
|
|
197
198
|
await git(author, ['init', '--bare', 'remote.git']);
|
|
198
199
|
await git(author, ['remote', 'add', 'origin', join(author, 'remote.git')]);
|
|
199
200
|
await git(author, ['push', 'origin', 'main']);
|
|
200
|
-
await
|
|
201
|
+
await git(author, ['clone', '--branch', 'main', join(author, 'remote.git'), 'runner']);
|
|
201
202
|
const runner = join(author, 'runner');
|
|
202
203
|
await git(runner, ['config', 'user.name', 'Test User']);
|
|
203
204
|
await git(runner, ['config', 'user.email', 'test@example.com']);
|
|
@@ -213,7 +214,7 @@ describe('release planning with fixture git repositories', () => {
|
|
|
213
214
|
|
|
214
215
|
expect(summary.pushed).toBe(true);
|
|
215
216
|
expect(shell.pushes).toEqual([['pushed@1.0.0']]);
|
|
216
|
-
await
|
|
217
|
+
await git(author, ['clone', '--branch', 'main', join(author, 'remote.git'), 'auditor']);
|
|
217
218
|
const auditor = join(author, 'auditor');
|
|
218
219
|
await git(auditor, ['fetch', '--tags', 'origin', 'main']);
|
|
219
220
|
await expect(gitOutput(auditor, ['rev-parse', 'refs/tags/pushed@1.0.0^{}'])).resolves.toBe(
|
|
@@ -237,7 +238,7 @@ describe('release planning with fixture git repositories', () => {
|
|
|
237
238
|
await git(author, ['init', '--bare', 'remote.git']);
|
|
238
239
|
await git(author, ['remote', 'add', 'origin', join(author, 'remote.git')]);
|
|
239
240
|
await git(author, ['push', 'origin', 'main', '--tags']);
|
|
240
|
-
await
|
|
241
|
+
await git(author, ['clone', '--branch', 'main', join(author, 'remote.git'), 'runner']);
|
|
241
242
|
const runner = join(author, 'runner');
|
|
242
243
|
await git(runner, ['config', 'user.name', 'Test User']);
|
|
243
244
|
await git(runner, ['config', 'user.email', 'test@example.com']);
|
|
@@ -323,7 +324,12 @@ class LocalGitRepairShell implements ReleaseRepairShell<ReleasePackageInfo> {
|
|
|
323
324
|
|
|
324
325
|
async buildReleaseCandidate(packages: ReleasePackageInfo[]): Promise<void> {
|
|
325
326
|
this.builds.push(packages.map((pkg) => pkg.name));
|
|
326
|
-
await
|
|
327
|
+
await runFixtureNx(this.root, [
|
|
328
|
+
'run-many',
|
|
329
|
+
'-t',
|
|
330
|
+
'build',
|
|
331
|
+
`--projects=${packages.map((pkg) => pkg.projectName).join(',')}`,
|
|
332
|
+
]);
|
|
327
333
|
}
|
|
328
334
|
|
|
329
335
|
async publishPackage(pkg: ReleasePackageInfo, distTag: string, dryRun: boolean): Promise<void> {
|
|
@@ -357,8 +363,3 @@ class LocalGitRepairShell implements ReleaseRepairShell<ReleasePackageInfo> {
|
|
|
357
363
|
await git(this.root, ['tag', '-a', tagName, '-m', tagName, 'HEAD']);
|
|
358
364
|
}
|
|
359
365
|
}
|
|
360
|
-
|
|
361
|
-
async function gitSucceeds(root: string, args: string[]): Promise<boolean> {
|
|
362
|
-
const result = await $`git ${args}`.cwd(root).quiet().nothrow();
|
|
363
|
-
return result.exitCode === 0;
|
|
364
|
-
}
|
|
@@ -4,6 +4,8 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { $ } from 'bun';
|
|
5
5
|
import type { GitReleaseTagInfo } from '../../core.js';
|
|
6
6
|
|
|
7
|
+
const GIT_TIMEOUT_MS = 10_000;
|
|
8
|
+
|
|
7
9
|
export async function withFixtureRepo(fn: (root: string) => Promise<void>): Promise<void> {
|
|
8
10
|
const root = await mkdtemp(join(tmpdir(), 'smoo-release-test-'));
|
|
9
11
|
try {
|
|
@@ -63,26 +65,98 @@ export async function writeBuildablePackage(
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export async function git(root: string, args: string[], env?: Record<string, string>): Promise<void> {
|
|
66
|
-
await
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
const result = await gitResult(root, args, env);
|
|
69
|
+
if (result.exitCode !== 0) {
|
|
70
|
+
throw new Error(gitErrorMessage(root, args, result));
|
|
71
|
+
}
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export async function gitOutput(root: string, args: string[]): Promise<string> {
|
|
73
|
-
const result = await
|
|
75
|
+
const result = await gitResult(root, args);
|
|
76
|
+
if (result.exitCode !== 0) {
|
|
77
|
+
throw new Error(gitErrorMessage(root, args, result));
|
|
78
|
+
}
|
|
74
79
|
return new TextDecoder().decode(result.stdout).trim();
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
export async function gitSucceeds(root: string, args: string[]): Promise<boolean> {
|
|
83
|
+
return (await gitResult(root, args)).exitCode === 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function gitResult(root: string, args: string[], env?: Record<string, string>): Promise<GitResult> {
|
|
87
|
+
const proc = Bun.spawn(['git', ...args], {
|
|
88
|
+
cwd: root,
|
|
89
|
+
env: { ...process.env, ...env },
|
|
90
|
+
stdout: 'pipe',
|
|
91
|
+
stderr: 'pipe',
|
|
92
|
+
});
|
|
93
|
+
let timedOut = false;
|
|
94
|
+
const timeout = setTimeout(() => {
|
|
95
|
+
timedOut = true;
|
|
96
|
+
proc.kill();
|
|
97
|
+
}, GIT_TIMEOUT_MS);
|
|
98
|
+
try {
|
|
99
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
100
|
+
proc.exited,
|
|
101
|
+
streamBytes(proc.stdout),
|
|
102
|
+
streamBytes(proc.stderr),
|
|
103
|
+
]);
|
|
104
|
+
return { exitCode, stdout, stderr, timedOut };
|
|
105
|
+
} finally {
|
|
106
|
+
clearTimeout(timeout);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function streamBytes(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
|
|
111
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function runFixtureNx(root: string, args: string[]): Promise<void> {
|
|
115
|
+
await $`nx ${args}`
|
|
116
|
+
.cwd(root)
|
|
117
|
+
.env({ ...definedProcessEnv(), NX_DAEMON: 'false' })
|
|
118
|
+
.quiet();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function definedProcessEnv(): Record<string, string> {
|
|
122
|
+
const env: Record<string, string> = {};
|
|
123
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
124
|
+
if (value !== undefined) {
|
|
125
|
+
env[key] = value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return env;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface GitResult {
|
|
132
|
+
exitCode: number;
|
|
133
|
+
stdout: Uint8Array;
|
|
134
|
+
stderr: Uint8Array;
|
|
135
|
+
timedOut: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function gitErrorMessage(root: string, args: string[], result: GitResult): string {
|
|
139
|
+
const stderr = new TextDecoder().decode(result.stderr).trim();
|
|
140
|
+
const timeoutText = result.timedOut
|
|
141
|
+
? ` timed out after ${GIT_TIMEOUT_MS}ms`
|
|
142
|
+
: ` failed with exit code ${result.exitCode}`;
|
|
143
|
+
return [`git ${args.join(' ')}${timeoutText}`, `cwd: ${root}`, stderr].filter(Boolean).join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
77
146
|
export async function tag(root: string, tagName: string, date: string): Promise<void> {
|
|
78
147
|
await git(root, ['tag', '-a', tagName, '-m', tagName], { GIT_COMMITTER_DATE: date });
|
|
79
148
|
}
|
|
80
149
|
|
|
81
150
|
export async function gitReleaseTagsByCreatorDate(root: string): Promise<GitReleaseTagInfo[]> {
|
|
82
|
-
const result =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
151
|
+
const result = await gitResult(root, [
|
|
152
|
+
'for-each-ref',
|
|
153
|
+
'--sort=-creatordate',
|
|
154
|
+
'--format=%(refname:short)%09%(creatordate:unix)%09%(*objectname)%09%(objectname)',
|
|
155
|
+
'refs/tags',
|
|
156
|
+
]);
|
|
157
|
+
if (result.exitCode !== 0) {
|
|
158
|
+
throw new Error(gitErrorMessage(root, ['for-each-ref', 'refs/tags'], result));
|
|
159
|
+
}
|
|
86
160
|
return new TextDecoder()
|
|
87
161
|
.decode(result.stdout)
|
|
88
162
|
.split('\n')
|
|
@@ -100,12 +174,11 @@ export async function gitReleaseTagsByCreatorDate(root: string): Promise<GitRele
|
|
|
100
174
|
}
|
|
101
175
|
|
|
102
176
|
export async function gitIsAncestor(root: string, ancestor: string, descendant: string): Promise<boolean> {
|
|
103
|
-
|
|
104
|
-
return result.exitCode === 0;
|
|
177
|
+
return gitSucceeds(root, ['merge-base', '--is-ancestor', ancestor, descendant]);
|
|
105
178
|
}
|
|
106
179
|
|
|
107
180
|
export async function packageVersionAtRef(root: string, packagePath: string, ref: string): Promise<string | null> {
|
|
108
|
-
const result = await
|
|
181
|
+
const result = await gitResult(root, ['show', `${ref}:${packagePath}/package.json`]);
|
|
109
182
|
if (result.exitCode !== 0) {
|
|
110
183
|
return null;
|
|
111
184
|
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- '**'
|
|
7
|
-
pull_request:
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
actions: read
|
|
11
|
-
contents: read
|
|
12
|
-
statuses: write
|
|
13
|
-
|
|
14
|
-
defaults:
|
|
15
|
-
run:
|
|
16
|
-
working-directory: tooling/direnv
|
|
17
|
-
|
|
18
|
-
jobs:
|
|
19
|
-
main:
|
|
20
|
-
name: Validate
|
|
21
|
-
runs-on: ubuntu-latest
|
|
22
|
-
timeout-minutes: 45
|
|
23
|
-
env:
|
|
24
|
-
NIX_STORE_NAR: ${{ github.workspace }}/nix-store.nar
|
|
25
|
-
GH_TOKEN: ${{ github.token }}
|
|
26
|
-
steps:
|
|
27
|
-
# Step 1: GitHub adds "Set up job" automatically
|
|
28
|
-
# Step 2
|
|
29
|
-
- name: ๐ฅ Checkout
|
|
30
|
-
uses: actions/checkout@v6.0.2
|
|
31
|
-
with:
|
|
32
|
-
filter: blob:none
|
|
33
|
-
fetch-depth: 0
|
|
34
|
-
|
|
35
|
-
# Step 3. Composite action internals do not affect top-level job step
|
|
36
|
-
# anchors; update the nx-smart --step values below if top-level steps move.
|
|
37
|
-
- name: ๐งฑ Setup Nix/devenv
|
|
38
|
-
id: setup
|
|
39
|
-
uses: ./.github/actions/setup-devenv
|
|
40
|
-
|
|
41
|
-
# --- Nx -----------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
# Step 4
|
|
44
|
-
# Sets the base and head SHAs required for the nx affected commands
|
|
45
|
-
- name: ๐งญ Set Nx SHAs
|
|
46
|
-
uses: nrwl/nx-set-shas@v5.0.1
|
|
47
|
-
with:
|
|
48
|
-
workflow-id: ci.yml
|
|
49
|
-
|
|
50
|
-
# Step 5
|
|
51
|
-
- name: ๐ง Restore Nx cache
|
|
52
|
-
id: nx-cache
|
|
53
|
-
uses: ./.github/actions/cache-nx
|
|
54
|
-
|
|
55
|
-
# Step 6
|
|
56
|
-
- name: ๐จ Build
|
|
57
|
-
run: smoo github-ci nx-smart --target build --name "Build" --step 6
|
|
58
|
-
|
|
59
|
-
# Step 7
|
|
60
|
-
- name: ๐ Lint
|
|
61
|
-
run: smoo github-ci nx-smart --target lint --name "Lint" --step 7
|
|
62
|
-
|
|
63
|
-
# Step 8
|
|
64
|
-
- name: ๐งช Unit Tests
|
|
65
|
-
run: smoo github-ci nx-smart --target test --name "Unit Tests" --step 8
|
|
66
|
-
|
|
67
|
-
# Step 9
|
|
68
|
-
# Nx's database cache needs artifact files and .nx/workspace-data DB
|
|
69
|
-
# metadata restored together; GitHub Actions cache is only the archive
|
|
70
|
-
# transport. Save runs only after build/lint/test succeed on the default
|
|
71
|
-
# branch, so PRs may restore shared cache but cannot publish it.
|
|
72
|
-
- name: ๐พ Save Nx cache
|
|
73
|
-
if:
|
|
74
|
-
${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}',
|
|
75
|
-
github.event.repository.default_branch) && steps.nx-cache.outputs.cache-hit != 'true' }}
|
|
76
|
-
uses: actions/cache/save@v5.0.5
|
|
77
|
-
with:
|
|
78
|
-
path: |
|
|
79
|
-
.nx/cache
|
|
80
|
-
.nx/workspace-data/*.db*
|
|
81
|
-
key: ${{ runner.os }}-nx-db-v1-${{ github.sha }}
|
|
82
|
-
|
|
83
|
-
# Step 10
|
|
84
|
-
- name: ๐ Upload trace DBs
|
|
85
|
-
if: ${{ always() }}
|
|
86
|
-
uses: actions/upload-artifact@v7.0.1
|
|
87
|
-
with:
|
|
88
|
-
name: trace-results-${{ github.run_id }}
|
|
89
|
-
path: packages/*/.trace-results.db
|
|
90
|
-
if-no-files-found: ignore
|
|
91
|
-
retention-days: 14
|
|
92
|
-
include-hidden-files: true
|
|
93
|
-
|
|
94
|
-
# --- Cleanup ------------------------------------------------------------
|
|
95
|
-
|
|
96
|
-
# Step 11
|
|
97
|
-
- name: ๐งน Cleanup and cache Nix/devenv
|
|
98
|
-
if: ${{ always() }}
|
|
99
|
-
uses: ./.github/actions/save-nix-devenv
|
|
100
|
-
with:
|
|
101
|
-
nix-cache-hit: ${{ steps.setup.outputs.nix-cache-hit }}
|
|
102
|
-
devenv-cache-hit: ${{ steps.setup.outputs.devenv-cache-hit }}
|