@smoothbricks/cli 0.7.0 → 0.9.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.
@@ -34,6 +34,82 @@ function splitLocalSection(current: string): { managed: string; localTail: strin
34
34
  /** Test seam for the pure splitter. */
35
35
  export const splitLocalSectionForTest = splitLocalSection;
36
36
 
37
+ /**
38
+ * A repo-owned block INSIDE the managed section — e.g. one extra pattern
39
+ * spliced into a formatter's list, where a trailing marker (LOCAL_SECTION_MARKER)
40
+ * can't express it because it isn't at the end of the file. Wrap it in
41
+ * `# smoo-local-begin` / `# smoo-local-end`; the block is anchored to the line
42
+ * immediately before `# smoo-local-begin`. On update, the block is re-spliced
43
+ * right after that same anchor line in the freshly rendered template — if the
44
+ * anchor no longer appears there (the template reworked that section), the
45
+ * update refuses rather than silently dropping the repo's customization.
46
+ */
47
+ export const INLINE_LOCAL_BEGIN = '# smoo-local-begin';
48
+ export const INLINE_LOCAL_END = '# smoo-local-end';
49
+
50
+ interface InlineLocalBlock {
51
+ anchor: string;
52
+ lines: string;
53
+ }
54
+
55
+ /** Pull inline local blocks out of a managed section, returning the section
56
+ * with each block (and its markers) removed, plus the extracted blocks in
57
+ * the order they appeared. */
58
+ function extractInlineLocalBlocks(managed: string): { withoutInline: string; blocks: InlineLocalBlock[] } {
59
+ const lines = managed.split('\n');
60
+ const kept: string[] = [];
61
+ const blocks: InlineLocalBlock[] = [];
62
+ let i = 0;
63
+ while (i < lines.length) {
64
+ const line = lines[i];
65
+ if (line !== undefined && line.trim() === INLINE_LOCAL_BEGIN) {
66
+ const anchor = kept.at(-1);
67
+ if (anchor === undefined) {
68
+ throw new Error(`${INLINE_LOCAL_BEGIN} on line ${i + 1} has no preceding anchor line`);
69
+ }
70
+ const blockLines: string[] = [];
71
+ i += 1;
72
+ while (i < lines.length && lines[i]?.trim() !== INLINE_LOCAL_END) {
73
+ blockLines.push(lines[i] as string);
74
+ i += 1;
75
+ }
76
+ if (i >= lines.length) {
77
+ throw new Error(`${INLINE_LOCAL_BEGIN} anchored on "${anchor}" has no matching ${INLINE_LOCAL_END}`);
78
+ }
79
+ blocks.push({ anchor, lines: blockLines.join('\n') });
80
+ i += 1; // skip the END marker line itself
81
+ continue;
82
+ }
83
+ kept.push(line);
84
+ i += 1;
85
+ }
86
+ return { withoutInline: kept.join('\n'), blocks };
87
+ }
88
+
89
+ /** Test seam for the pure extractor. */
90
+ export const extractInlineLocalBlocksForTest = extractInlineLocalBlocks;
91
+
92
+ /** Re-splice extracted inline blocks into freshly rendered managed content,
93
+ * each immediately after its anchor line. A no-op when there are no blocks. */
94
+ function reinsertInlineLocalBlocks(content: string, blocks: InlineLocalBlock[]): string {
95
+ if (blocks.length === 0) return content;
96
+ const lines = content.split('\n');
97
+ for (const block of blocks) {
98
+ const index = lines.indexOf(block.anchor);
99
+ if (index === -1) {
100
+ throw new Error(
101
+ `${INLINE_LOCAL_BEGIN} block anchored on "${block.anchor}" no longer matches any line in the updated ` +
102
+ 'template — reconcile the repo-owned block manually',
103
+ );
104
+ }
105
+ lines.splice(index + 1, 0, INLINE_LOCAL_BEGIN, ...block.lines.split('\n'), INLINE_LOCAL_END);
106
+ }
107
+ return lines.join('\n');
108
+ }
109
+
110
+ /** Test seam for the pure re-splicer. */
111
+ export const reinsertInlineLocalBlocksForTest = reinsertInlineLocalBlocks;
112
+
37
113
  export interface FileResult {
38
114
  target: string;
39
115
  action: 'created' | 'updated' | 'unchanged' | 'skipped' | 'skipped-symlink' | 'drifted' | 'ok-symlink';
@@ -173,13 +249,15 @@ function applyManagedFile(
173
249
  }
174
250
  const current = readFileSync(target, 'utf8');
175
251
  const { managed, localTail } = splitLocalSection(current);
176
- if (managed === content || (localTail !== '' && managed === `${content}\n`)) {
252
+ const { withoutInline, blocks } = extractInlineLocalBlocks(managed);
253
+ if (withoutInline === content || (localTail !== '' && withoutInline === `${content}\n`)) {
177
254
  return { target: file.target, action: 'unchanged' };
178
255
  }
179
256
  if (mode === 'check' || mode === 'diff') {
180
257
  return { target: file.target, action: 'drifted' };
181
258
  }
182
- const next = localTail === '' ? content : `${content}\n${localTail}`;
259
+ const rendered = reinsertInlineLocalBlocks(content, blocks);
260
+ const next = localTail === '' ? rendered : `${rendered}\n${localTail}`;
183
261
  writeManagedFile(target, next, file.executable === true);
184
262
  return { target: file.target, action: 'updated' };
185
263
  }
@@ -26,6 +26,7 @@ import {
26
26
  } from '../packed-package.js';
27
27
  import { syncRootRuntimeVersions } from '../runtime.js';
28
28
  import { applyToolConfigDefaults, validateToolConfig } from '../tool-validation.js';
29
+ import { applyWranglerDefaults, validateWrangler } from '../wrangler.js';
29
30
 
30
31
  export interface MonorepoContext {
31
32
  root: string;
@@ -150,6 +151,18 @@ const packs: MonorepoPack[] = [
150
151
  return validatePackedPublicPackageTypes(ctx.root);
151
152
  },
152
153
  },
154
+ {
155
+ name: 'wrangler',
156
+ init(ctx) {
157
+ applyWranglerDefaults(ctx.root);
158
+ },
159
+ fixPreBuild(ctx) {
160
+ applyWranglerDefaults(ctx.root);
161
+ },
162
+ validatePreBuild(ctx) {
163
+ return validateWrangler(ctx.root);
164
+ },
165
+ },
153
166
  ];
154
167
 
155
168
  export async function runInitPacks(ctx: MonorepoContext): Promise<void> {
@@ -0,0 +1,216 @@
1
+ import { describe, expect, it, spyOn } from 'bun:test';
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { applyWranglerDefaults, firstWranglerEnv, validateWrangler } from './wrangler.js';
6
+
7
+ describe('firstWranglerEnv', () => {
8
+ it('extracts the first [env.<name>] header', () => {
9
+ expect(firstWranglerEnv('name = "svc"\n\n[env.staging]\nvars = {}\n\n[env.production]\n')).toBe('staging');
10
+ });
11
+
12
+ it('handles nested env sub-tables', () => {
13
+ expect(firstWranglerEnv('[env.production.vars]\nFOO = "bar"\n')).toBe('production');
14
+ });
15
+
16
+ it('returns null when the toml declares no env block', () => {
17
+ expect(firstWranglerEnv('name = "svc"\nmain = "src/index.ts"\n')).toBeNull();
18
+ });
19
+ });
20
+
21
+ describe('applyWranglerDefaults', () => {
22
+ it('injects the wrangler-types target and wires typecheck, idempotently', async () => {
23
+ const root = await createWorkspace([
24
+ { dir: 'api', name: '@acme/api', toml: '[env.production]\n', nx: { targets: { typecheck: { dependsOn: ['^build'] } } } },
25
+ ]);
26
+ try {
27
+ applyWranglerDefaults(root);
28
+ const pkg = await readJson(join(root, 'packages/api/package.json'));
29
+ expect(nxTargets(pkg)['wrangler-types']).toEqual({
30
+ executor: 'nx:run-commands',
31
+ cache: true,
32
+ inputs: ['{projectRoot}/wrangler.toml', '{projectRoot}/.dev.vars.example'],
33
+ outputs: ['{projectRoot}/worker-configuration.d.ts'],
34
+ options: {
35
+ command: 'wrangler types --env production --env-file .dev.vars.example --include-runtime false',
36
+ cwd: '{projectRoot}',
37
+ },
38
+ });
39
+ const typecheck = nxTargets(pkg).typecheck;
40
+ expect(isRecord(typecheck) ? typecheck.dependsOn : null).toEqual(['^build', 'wrangler-types']);
41
+
42
+ const logs = captureConsoleLogs();
43
+ applyWranglerDefaults(root);
44
+ expect(logs.some((line) => line.startsWith('updated'))).toBe(false);
45
+ expect(logs.some((line) => line.startsWith('unchanged'))).toBe(true);
46
+ expect(await readJson(join(root, 'packages/api/package.json'))).toEqual(pkg);
47
+ } finally {
48
+ console.log = originalConsoleLog;
49
+ await rm(root, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ it('omits --env from the command when the wrangler.toml declares no env block', async () => {
54
+ const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\nmain = "src/index.ts"\n' }]);
55
+ captureConsoleLogs();
56
+ try {
57
+ applyWranglerDefaults(root);
58
+ const target = nxTargets(await readJson(join(root, 'packages/api/package.json')))['wrangler-types'];
59
+ const options = isRecord(target) ? target.options : null;
60
+ expect(isRecord(options) ? options.command : null).toBe(
61
+ 'wrangler types --env-file .dev.vars.example --include-runtime false',
62
+ );
63
+ } finally {
64
+ console.log = originalConsoleLog;
65
+ await rm(root, { recursive: true, force: true });
66
+ }
67
+ });
68
+
69
+ it('creates an empty .dev.vars.example and logs it when the project has none', async () => {
70
+ const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\n' }]);
71
+ const logs = captureConsoleLogs();
72
+ try {
73
+ applyWranglerDefaults(root);
74
+ expect(await readFile(join(root, 'packages/api/.dev.vars.example'), 'utf8')).toBe('');
75
+ expect(logs.some((line) => line.startsWith('created') && line.includes('api/.dev.vars.example'))).toBe(true);
76
+ } finally {
77
+ console.log = originalConsoleLog;
78
+ await rm(root, { recursive: true, force: true });
79
+ }
80
+ });
81
+
82
+ it('preserves a human-authored .dev.vars.example and does not log a creation', async () => {
83
+ const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\n' }]);
84
+ const examplePath = join(root, 'packages/api/.dev.vars.example');
85
+ await writeFile(examplePath, 'GITHUB_CLIENT_SECRET=\n');
86
+ const logs = captureConsoleLogs();
87
+ try {
88
+ applyWranglerDefaults(root);
89
+ expect(await readFile(examplePath, 'utf8')).toBe('GITHUB_CLIENT_SECRET=\n');
90
+ expect(logs.some((line) => line.startsWith('created') && line.includes('.dev.vars.example'))).toBe(false);
91
+ } finally {
92
+ console.log = originalConsoleLog;
93
+ await rm(root, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ it('adds the missing entries to an incomplete root .gitignore', async () => {
98
+ const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\n' }]);
99
+ const logs = captureConsoleLogs();
100
+ try {
101
+ await writeFile(join(root, '.gitignore'), 'node_modules\n');
102
+ applyWranglerDefaults(root);
103
+ const lines = (await readFile(join(root, '.gitignore'), 'utf8')).split('\n').map((line) => line.trim());
104
+ expect(lines).toContain('.dev.vars');
105
+ expect(lines).toContain('worker-configuration.d.ts');
106
+ expect(logs.some((line) => line.startsWith('updated') && line.includes('.gitignore'))).toBe(true);
107
+ } finally {
108
+ console.log = originalConsoleLog;
109
+ await rm(root, { recursive: true, force: true });
110
+ }
111
+ });
112
+
113
+ it('leaves the root .gitignore untouched when both entries are already present', async () => {
114
+ const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\n' }]);
115
+ const gitignorePath = join(root, '.gitignore');
116
+ const logs = captureConsoleLogs();
117
+ try {
118
+ await writeFile(gitignorePath, 'node_modules\n.dev.vars\nworker-configuration.d.ts\n');
119
+ const before = await readFile(gitignorePath, 'utf8');
120
+ applyWranglerDefaults(root);
121
+ expect(await readFile(gitignorePath, 'utf8')).toBe(before);
122
+ expect(logs.some((line) => line.startsWith('updated') && line.includes('.gitignore'))).toBe(false);
123
+ } finally {
124
+ console.log = originalConsoleLog;
125
+ await rm(root, { recursive: true, force: true });
126
+ }
127
+ });
128
+ });
129
+
130
+ describe('validateWrangler', () => {
131
+ it('flags a project whose .dev.vars.example is missing while target and .gitignore are wired', async () => {
132
+ const root = await createWorkspace([
133
+ {
134
+ dir: 'api',
135
+ name: '@acme/api',
136
+ toml: '[env.staging]\n',
137
+ nx: { targets: { 'wrangler-types': { executor: 'nx:run-commands' } } },
138
+ },
139
+ ]);
140
+ try {
141
+ await writeFile(join(root, '.gitignore'), '.dev.vars\nworker-configuration.d.ts\n');
142
+ expect(validateWrangler(root)).toBe(1);
143
+ } finally {
144
+ await rm(root, { recursive: true, force: true });
145
+ }
146
+ });
147
+
148
+ it('passes a fully wired project', async () => {
149
+ const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: '[env.staging]\n' }]);
150
+ try {
151
+ applyWranglerDefaults(root);
152
+ await writeFile(join(root, 'packages/api/.dev.vars.example'), 'SECRET=\n');
153
+ await writeFile(join(root, '.gitignore'), '.dev.vars\n!.dev.vars.example\nworker-configuration.d.ts\n');
154
+ expect(validateWrangler(root)).toBe(0);
155
+ } finally {
156
+ await rm(root, { recursive: true, force: true });
157
+ }
158
+ });
159
+ });
160
+
161
+ async function createWorkspace(
162
+ packages: Array<{ dir: string; name: string; toml: string; nx?: Record<string, unknown> }>,
163
+ ): Promise<string> {
164
+ const root = await mkdtemp(join(tmpdir(), 'smoo-wrangler-'));
165
+ await writeJson(join(root, 'package.json'), {
166
+ name: '@acme/codebase',
167
+ version: '0.0.0',
168
+ private: true,
169
+ workspaces: ['packages/*'],
170
+ });
171
+ for (const pkg of packages) {
172
+ await writeJson(join(root, `packages/${pkg.dir}/package.json`), {
173
+ name: pkg.name,
174
+ version: '0.0.0',
175
+ private: true,
176
+ ...(pkg.nx ? { nx: pkg.nx } : {}),
177
+ });
178
+ await writeFile(join(root, `packages/${pkg.dir}/wrangler.toml`), pkg.toml);
179
+ }
180
+ return root;
181
+ }
182
+
183
+ function nxTargets(pkg: Record<string, unknown>): Record<string, unknown> {
184
+ const nx = pkg.nx;
185
+ if (!isRecord(nx) || !isRecord(nx.targets)) {
186
+ throw new Error('nx.targets not found');
187
+ }
188
+ return nx.targets;
189
+ }
190
+
191
+ function isRecord(value: unknown): value is Record<string, unknown> {
192
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
193
+ }
194
+
195
+ async function readJson(path: string): Promise<Record<string, unknown>> {
196
+ const parsed: unknown = JSON.parse(await readFile(path, 'utf8'));
197
+ if (!isRecord(parsed)) {
198
+ throw new Error('expected JSON object');
199
+ }
200
+ return parsed;
201
+ }
202
+
203
+ async function writeJson(path: string, value: Record<string, unknown>): Promise<void> {
204
+ await mkdir(join(path, '..'), { recursive: true });
205
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
206
+ }
207
+
208
+ const originalConsoleLog = console.log;
209
+
210
+ function captureConsoleLogs(): string[] {
211
+ const logs: string[] = [];
212
+ spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
213
+ logs.push(args.join(' '));
214
+ });
215
+ return logs;
216
+ }
@@ -0,0 +1,135 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { getOrCreateRecord, recordProperty, writeJsonObject } from '../lib/json.js';
4
+ import { getWorkspacePackageManifests } from '../lib/workspace.js';
5
+
6
+ interface WranglerProject {
7
+ label: string;
8
+ dir: string;
9
+ packageJsonPath: string;
10
+ json: Record<string, unknown>;
11
+ tomlPath: string;
12
+ }
13
+
14
+ /** First `[env.<name>]` header in a wrangler.toml, or `null` when the config declares no envs (top-level bindings only). */
15
+ export function firstWranglerEnv(tomlText: string): string | null {
16
+ const match = tomlText.match(/^\s*\[env\.([A-Za-z0-9_-]+)/m);
17
+ return match ? match[1] : null;
18
+ }
19
+
20
+ function wranglerProjects(root: string): WranglerProject[] {
21
+ const projects: WranglerProject[] = [];
22
+ for (const pkg of getWorkspacePackageManifests(root)) {
23
+ const tomlPath = join(pkg.path, 'wrangler.toml');
24
+ if (!existsSync(tomlPath)) {
25
+ continue;
26
+ }
27
+ projects.push({
28
+ label: relative(root, pkg.path) || '.',
29
+ dir: pkg.path,
30
+ packageJsonPath: pkg.packageJsonPath,
31
+ json: pkg.json,
32
+ tomlPath,
33
+ });
34
+ }
35
+ return projects;
36
+ }
37
+
38
+ /** Ensure the root .gitignore ignores the local secret file + generated types (once). */
39
+ function ensureGitignore(root: string): void {
40
+ const path = join(root, '.gitignore');
41
+ const text = existsSync(path) ? readFileSync(path, 'utf8') : '';
42
+ const lines = text.split('\n').map((line) => line.trim());
43
+ const missing = ['.dev.vars', 'worker-configuration.d.ts'].filter((entry) => !lines.includes(entry));
44
+ if (missing.length === 0) {
45
+ return;
46
+ }
47
+ const block = `${text.replace(/\s*$/, '')}\n\n# wrangler: local secret values + generated types (names live in .dev.vars.example)\n${missing.join('\n')}\n`;
48
+ writeFileSync(path, block);
49
+ console.log(`updated .gitignore (${missing.join(', ')})`);
50
+ }
51
+
52
+ export function applyWranglerDefaults(root: string): void {
53
+ const projects = wranglerProjects(root);
54
+ if (projects.length > 0) {
55
+ ensureGitignore(root);
56
+ }
57
+ for (const project of projects) {
58
+ const env = firstWranglerEnv(readFileSync(project.tomlPath, 'utf8'));
59
+ const nx = getOrCreateRecord(project.json, 'nx');
60
+ const targets = getOrCreateRecord(nx, 'targets');
61
+ const desired: Record<string, unknown> = {
62
+ executor: 'nx:run-commands',
63
+ cache: true,
64
+ inputs: ['{projectRoot}/wrangler.toml', '{projectRoot}/.dev.vars.example'],
65
+ outputs: ['{projectRoot}/worker-configuration.d.ts'],
66
+ options: {
67
+ command: `wrangler types${env ? ` --env ${env}` : ''} --env-file .dev.vars.example --include-runtime false`,
68
+ cwd: '{projectRoot}',
69
+ },
70
+ };
71
+ let changed = false;
72
+ const existing = recordProperty(targets, 'wrangler-types');
73
+ if (!existing || JSON.stringify(existing) !== JSON.stringify(desired)) {
74
+ targets['wrangler-types'] = desired;
75
+ changed = true;
76
+ }
77
+ const typecheck = recordProperty(targets, 'typecheck');
78
+ if (typecheck) {
79
+ const dependsOn: unknown[] = Array.isArray(typecheck.dependsOn) ? typecheck.dependsOn : [];
80
+ if (!dependsOn.includes('wrangler-types')) {
81
+ dependsOn.push('wrangler-types');
82
+ typecheck.dependsOn = dependsOn;
83
+ changed = true;
84
+ }
85
+ }
86
+ if (changed) {
87
+ writeJsonObject(project.packageJsonPath, project.json);
88
+ console.log(`updated ${project.label}/package.json wrangler-types target`);
89
+ } else {
90
+ console.log(`unchanged ${project.label}/package.json wrangler-types target`);
91
+ }
92
+ // Empty .dev.vars.example = "this worker declares no secrets". Bootstrap it
93
+ // when missing; humans add SECRET_NAME= lines if the worker reads any (the
94
+ // wrangler-types + tsc gate fails on an undeclared secret until they do).
95
+ const examplePath = join(project.dir, '.dev.vars.example');
96
+ if (!existsSync(examplePath)) {
97
+ writeFileSync(examplePath, '');
98
+ console.log(`created ${project.label}/.dev.vars.example (no secrets)`);
99
+ }
100
+ }
101
+ }
102
+
103
+ export function validateWrangler(root: string): number {
104
+ const gitignorePath = join(root, '.gitignore');
105
+ const gitignoreLines = existsSync(gitignorePath)
106
+ ? readFileSync(gitignorePath, 'utf8')
107
+ .split('\n')
108
+ .map((line) => line.trim())
109
+ : [];
110
+ const gitignoreCoversSecrets =
111
+ gitignoreLines.includes('.dev.vars') && gitignoreLines.includes('worker-configuration.d.ts');
112
+ let problems = 0;
113
+ for (const project of wranglerProjects(root)) {
114
+ let projectProblems = 0;
115
+ if (!existsSync(join(project.dir, '.dev.vars.example'))) {
116
+ console.log(`⨯ ${project.label}: missing .dev.vars.example`);
117
+ projectProblems++;
118
+ }
119
+ const nx = recordProperty(project.json, 'nx');
120
+ const targets = nx ? recordProperty(nx, 'targets') : null;
121
+ if (!targets || !recordProperty(targets, 'wrangler-types')) {
122
+ console.log(`⨯ ${project.label}: missing wrangler-types nx target`);
123
+ projectProblems++;
124
+ }
125
+ if (!gitignoreCoversSecrets) {
126
+ console.log(`⨯ ${project.label}: root .gitignore must ignore .dev.vars and worker-configuration.d.ts`);
127
+ projectProblems++;
128
+ }
129
+ if (projectProblems === 0) {
130
+ console.log(`✓ ${project.label}: wrangler config`);
131
+ }
132
+ problems += projectProblems;
133
+ }
134
+ return problems;
135
+ }
@@ -6,6 +6,24 @@ import type { GitReleaseTagInfo } from '../../core.js';
6
6
 
7
7
  const GIT_TIMEOUT_MS = 10_000;
8
8
 
9
+ // Isolate fixture git from the runner environment and skip fsync. These are
10
+ // throwaway temp repos (deleted in withFixtureRepo's finally) so durability is
11
+ // irrelevant — fsync is pure cost. On GitHub Actions the ~20 sequential git
12
+ // spawns in a push test otherwise sum past the 30s cap: fsync on the runner's
13
+ // disk is the dominant cost, and inheriting the runner's global/system config
14
+ // (credential helpers, url.*.insteadOf rewrites, fsmonitor) slows every spawn
15
+ // and risks a credential-prompt hang. Unknown core.* keys are ignored by older
16
+ // git, so this is a harmless speedup locally and the real fix on CI.
17
+ const GIT_FIXTURE_ENV: Record<string, string> = {
18
+ GIT_CONFIG_GLOBAL: '/dev/null',
19
+ GIT_CONFIG_SYSTEM: '/dev/null',
20
+ GIT_TERMINAL_PROMPT: '0',
21
+ GIT_OPTIONAL_LOCKS: '0',
22
+ GIT_CONFIG_COUNT: '1',
23
+ GIT_CONFIG_KEY_0: 'core.fsync',
24
+ GIT_CONFIG_VALUE_0: 'none',
25
+ };
26
+
9
27
  export async function withFixtureRepo(fn: (root: string) => Promise<void>): Promise<void> {
10
28
  const root = await mkdtemp(join(tmpdir(), 'smoo-release-test-'));
11
29
  try {
@@ -86,7 +104,7 @@ export async function gitSucceeds(root: string, args: string[]): Promise<boolean
86
104
  async function gitResult(root: string, args: string[], env?: Record<string, string>): Promise<GitResult> {
87
105
  const proc = Bun.spawn(['git', ...args], {
88
106
  cwd: root,
89
- env: { ...process.env, ...env },
107
+ env: { ...process.env, ...GIT_FIXTURE_ENV, ...env },
90
108
  stdout: 'pipe',
91
109
  stderr: 'pipe',
92
110
  });