@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.
@@ -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,CA2BzE"}
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"}
@@ -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 = setStringProperty(devDependencies, '@types/node', `~${nodeMajor}.0.0`) || changed;
25
- changed = setStringProperty(devDependencies, '@types/bun', bunVersion) || changed;
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",
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.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: 'bun test',
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: 'bun test --pass-with-no-tests',
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: 'bun test --pass-with-no-tests',
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
+ });
@@ -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 = setStringProperty(devDependencies, '@types/node', `~${nodeMajor}.0.0`) || changed;
27
- changed = setStringProperty(devDependencies, '@types/bun', bunVersion) || changed;
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 $`git clone --branch main ${join(root, 'remote.git')} checkout`.cwd(root).quiet();
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 $`nx run-many -t build --projects=${'a,b'}`.cwd(root).quiet();
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 $`git clone --branch main ${join(author, 'remote.git')} runner`.cwd(author).quiet();
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 $`git clone --branch main ${join(author, 'remote.git')} runner`.cwd(author).quiet();
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 $`git clone --branch main ${join(author, 'remote.git')} auditor`.cwd(author).quiet();
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 $`git clone --branch main ${join(author, 'remote.git')} runner`.cwd(author).quiet();
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 $`nx run-many -t build --projects=${packages.map((pkg) => pkg.projectName).join(',')}`.cwd(this.root).quiet();
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 $`git ${args}`
67
- .cwd(root)
68
- .env(env ?? {})
69
- .quiet();
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 $`git ${args}`.cwd(root).quiet();
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
- await $`git for-each-ref --sort=-creatordate --format=${'%(refname:short)%09%(creatordate:unix)%09%(*objectname)%09%(objectname)'} refs/tags`
84
- .cwd(root)
85
- .quiet();
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
- const result = await $`git merge-base --is-ancestor ${ancestor} ${descendant}`.cwd(root).quiet().nothrow();
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 $`git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
181
+ const result = await gitResult(root, ['show', `${ref}:${packagePath}/package.json`]);
109
182
  if (result.exitCode !== 0) {
110
183
  return null;
111
184
  }