@smoothbricks/cli 0.3.3 → 0.3.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/dist/monorepo/runtime.d.ts +3 -0
- package/dist/monorepo/runtime.d.ts.map +1 -1
- package/dist/monorepo/runtime.js +66 -2
- package/package.json +3 -3
- package/src/monorepo/package-policy.test.ts +8 -3
- 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
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export declare function syncRootRuntimeVersions(root: string): Promise<void>;
|
|
2
|
+
type RuntimeTypesPinMode = 'major' | 'exact';
|
|
3
|
+
export declare function runtimeTypesRangeForPublishedVersions(packageName: string, runtimeVersion: string, pinMode: RuntimeTypesPinMode, versions: readonly string[]): string;
|
|
4
|
+
export {};
|
|
2
5
|
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/monorepo/runtime.ts"],"names":[],"mappings":"AAKA,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/monorepo/runtime.ts"],"names":[],"mappings":"AAKA,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCzE;AAED,KAAK,mBAAmB,GAAG,OAAO,GAAG,OAAO,CAAC;AAgB7C,wBAAgB,qCAAqC,CACnD,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,MAAM,CAsBR"}
|
package/dist/monorepo/runtime.js
CHANGED
|
@@ -21,13 +21,77 @@ export async function syncRootRuntimeVersions(root) {
|
|
|
21
21
|
changed = setStringProperty(engines, 'node', `>=${nodeMajor}.0.0`) || changed;
|
|
22
22
|
changed = setStringProperty(packageJson, 'packageManager', `bun@${bunVersion}`) || changed;
|
|
23
23
|
const devDependencies = getOrCreateRecord(packageJson, 'devDependencies');
|
|
24
|
-
changed =
|
|
25
|
-
|
|
24
|
+
changed =
|
|
25
|
+
setStringProperty(devDependencies, '@types/node', await runtimeTypesRange(root, '@types/node', nodeVersion, 'major')) || changed;
|
|
26
|
+
changed =
|
|
27
|
+
setStringProperty(devDependencies, '@types/bun', await runtimeTypesRange(root, '@types/bun', bunVersion, 'exact')) || changed;
|
|
26
28
|
if (changed) {
|
|
27
29
|
writeJsonObject(packageJsonPath, packageJson);
|
|
28
30
|
console.log('updated package.json runtime versions');
|
|
29
31
|
}
|
|
30
32
|
}
|
|
33
|
+
async function runtimeTypesRange(root, packageName, runtimeVersion, pinMode) {
|
|
34
|
+
const versionsText = await $ `bun pm view ${packageName} versions --json`.cwd(root).text();
|
|
35
|
+
const versions = JSON.parse(versionsText);
|
|
36
|
+
if (!Array.isArray(versions) || !versions.every((version) => typeof version === 'string')) {
|
|
37
|
+
throw new Error(`Unable to read published ${packageName} versions`);
|
|
38
|
+
}
|
|
39
|
+
return runtimeTypesRangeForPublishedVersions(packageName, runtimeVersion, pinMode, versions);
|
|
40
|
+
}
|
|
41
|
+
export function runtimeTypesRangeForPublishedVersions(packageName, runtimeVersion, pinMode, versions) {
|
|
42
|
+
const parsedRuntimeVersion = parseVersion(runtimeVersion);
|
|
43
|
+
if (!parsedRuntimeVersion) {
|
|
44
|
+
throw new Error(`Unable to parse runtime version ${runtimeVersion}`);
|
|
45
|
+
}
|
|
46
|
+
if (pinMode === 'major') {
|
|
47
|
+
const runtimeMajor = parsedRuntimeVersion[0].toString();
|
|
48
|
+
if (latestVersion(versions.filter((version) => versionMajor(version) === runtimeMajor))) {
|
|
49
|
+
return `~${runtimeMajor}.0.0`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (versions.includes(runtimeVersion)) {
|
|
53
|
+
return runtimeVersion;
|
|
54
|
+
}
|
|
55
|
+
const latest = latestVersion(versions);
|
|
56
|
+
if (!latest) {
|
|
57
|
+
throw new Error(`Unable to find any published ${packageName} versions`);
|
|
58
|
+
}
|
|
59
|
+
console.warn(`${packageName} has no published types for runtime ${runtimeVersion}; using ${latest}`);
|
|
60
|
+
return pinMode === 'major' ? `~${latest}` : latest;
|
|
61
|
+
}
|
|
62
|
+
function latestVersion(versions) {
|
|
63
|
+
let latest = null;
|
|
64
|
+
for (const version of versions) {
|
|
65
|
+
if (!parseVersion(version)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!latest || compareVersions(version, latest) > 0) {
|
|
69
|
+
latest = version;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return latest;
|
|
73
|
+
}
|
|
74
|
+
function compareVersions(left, right) {
|
|
75
|
+
const leftParts = parseVersion(left);
|
|
76
|
+
const rightParts = parseVersion(right);
|
|
77
|
+
if (!leftParts || !rightParts) {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
for (let index = 0; index < leftParts.length; index++) {
|
|
81
|
+
const diff = leftParts[index] - rightParts[index];
|
|
82
|
+
if (diff !== 0) {
|
|
83
|
+
return diff;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
function versionMajor(version) {
|
|
89
|
+
return parseVersion(version)?.[0].toString() ?? null;
|
|
90
|
+
}
|
|
91
|
+
function parseVersion(version) {
|
|
92
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
93
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
94
|
+
}
|
|
31
95
|
function runtimeCommand(root, name) {
|
|
32
96
|
const candidates = [
|
|
33
97
|
join(root, 'tooling', 'direnv', '.devenv', 'profile', 'bin', name),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smoothbricks/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SmoothBricks monorepo automation CLI",
|
|
6
6
|
"bin": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@arethetypeswrong/cli": "^0.18.2",
|
|
48
|
-
"@smoothbricks/nx-plugin": "0.2.
|
|
48
|
+
"@smoothbricks/nx-plugin": "0.2.3",
|
|
49
49
|
"@smoothbricks/validation": "0.1.1",
|
|
50
50
|
"commander": "^14.0.3",
|
|
51
51
|
"publint": "^0.3.18",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"test": {
|
|
73
73
|
"executor": "@smoothbricks/nx-plugin:bounded-exec",
|
|
74
74
|
"options": {
|
|
75
|
-
"command": "bun test",
|
|
75
|
+
"command": "bun test --timeout=30000",
|
|
76
76
|
"cwd": "{projectRoot}",
|
|
77
77
|
"timeoutMs": 120000,
|
|
78
78
|
"killAfterMs": 1e4
|
|
@@ -5,8 +5,12 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import {
|
|
6
6
|
BOUNDED_TEST_EXECUTOR,
|
|
7
7
|
BOUNDED_TEST_KILL_AFTER_MS,
|
|
8
|
+
BOUNDED_TEST_PER_TEST_TIMEOUT_MS,
|
|
8
9
|
BOUNDED_TEST_TIMEOUT_MS,
|
|
9
10
|
} from '@smoothbricks/nx-plugin/bounded-test-policy';
|
|
11
|
+
|
|
12
|
+
const TIMEOUT_FLAG = `--timeout=${BOUNDED_TEST_PER_TEST_TIMEOUT_MS}`;
|
|
13
|
+
|
|
10
14
|
import {
|
|
11
15
|
applyFixableMonorepoDefaults,
|
|
12
16
|
applyNxPluginDefaults,
|
|
@@ -595,7 +599,7 @@ describe('workspace package script policy', () => {
|
|
|
595
599
|
test: {
|
|
596
600
|
executor: BOUNDED_TEST_EXECUTOR,
|
|
597
601
|
options: {
|
|
598
|
-
command:
|
|
602
|
+
command: `bun test ${TIMEOUT_FLAG}`,
|
|
599
603
|
cwd: '{projectRoot}',
|
|
600
604
|
timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
|
|
601
605
|
killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
|
|
@@ -825,7 +829,7 @@ describe('workspace package script policy', () => {
|
|
|
825
829
|
test: {
|
|
826
830
|
executor: BOUNDED_TEST_EXECUTOR,
|
|
827
831
|
options: {
|
|
828
|
-
command:
|
|
832
|
+
command: `bun test ${TIMEOUT_FLAG} --pass-with-no-tests`,
|
|
829
833
|
cwd: '{projectRoot}',
|
|
830
834
|
timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
|
|
831
835
|
killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
|
|
@@ -970,7 +974,7 @@ describe('workspace package script policy', () => {
|
|
|
970
974
|
test: {
|
|
971
975
|
executor: BOUNDED_TEST_EXECUTOR,
|
|
972
976
|
options: {
|
|
973
|
-
command:
|
|
977
|
+
command: `bun test ${TIMEOUT_FLAG} --pass-with-no-tests`,
|
|
974
978
|
cwd: '{projectRoot}',
|
|
975
979
|
timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
|
|
976
980
|
killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
|
|
@@ -1076,6 +1080,7 @@ function validNxJson(): Record<string, unknown> {
|
|
|
1076
1080
|
},
|
|
1077
1081
|
releaseTag: { pattern: '{projectName}@{version}' },
|
|
1078
1082
|
changelog: {
|
|
1083
|
+
automaticFromRef: true,
|
|
1079
1084
|
workspaceChangelog: false,
|
|
1080
1085
|
projectChangelogs: {
|
|
1081
1086
|
createRelease: false,
|
|
@@ -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
|
}
|