Skip to content

Commit 5402897

Browse files
centdixclaude
andauthored
fix: reload dotenv after post-create hook (#111)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2fc6722 commit 5402897

2 files changed

Lines changed: 74 additions & 12 deletions

File tree

backend/src/__tests__/lifecycle-service.test.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ function run(args: string[], cwd: string): string {
3131

3232
class FakeTmuxGateway implements TmuxGateway {
3333
private readonly windows = new Map<string, TmuxWindowSummary>();
34+
readonly commands: Array<{ target: string; command: string }> = [];
3435

3536
ensureServer(): void {}
3637

@@ -69,7 +70,9 @@ class FakeTmuxGateway implements TmuxGateway {
6970

7071
setWindowOption(_sessionName: string, _windowName: string, _option: string, _value: string): void {}
7172

72-
runCommand(_target: string, _command: string): void {}
73+
runCommand(target: string, command: string): void {
74+
this.commands.push({ target, command });
75+
}
7376

7477
selectPane(_target: string): void {}
7578

@@ -110,15 +113,15 @@ class FakeHookRunner implements LifecycleHookRunner {
110113
readonly calls: RunLifecycleHookInput[] = [];
111114

112115
constructor(
113-
private readonly onRun?: (input: RunLifecycleHookInput) => void,
116+
private readonly onRun?: (input: RunLifecycleHookInput) => void | Promise<void>,
114117
) {}
115118

116119
async run(input: RunLifecycleHookInput): Promise<void> {
117120
this.calls.push({
118121
...input,
119122
env: { ...input.env },
120123
});
121-
this.onRun?.(input);
124+
await this.onRun?.(input);
122125
}
123126
}
124127

@@ -340,6 +343,47 @@ describe("LifecycleService", () => {
340343
expect(state?.session.paneCount).toBe(2);
341344
});
342345

346+
it("refreshes runtime env after postCreate so system prompts see .env.local values", async () => {
347+
const repoRoot = await initRepo();
348+
const runtime = new ProjectRuntime();
349+
const tmux = new FakeTmuxGateway();
350+
const databaseUrl = "postgres://postgres:changeme@127.0.0.1:5432/windmill_feature_prompt?sslmode=disable";
351+
const hooks = new FakeHookRunner(async (input) => {
352+
await Bun.write(join(input.cwd, ".env.local"), `DATABASE_URL=${databaseUrl}\n`);
353+
});
354+
const lifecycle = makeLifecycleService(
355+
repoRoot,
356+
tmux,
357+
runtime,
358+
new FakeDockerGateway(),
359+
hooks,
360+
{
361+
...TEST_CONFIG,
362+
profiles: {
363+
...TEST_CONFIG.profiles,
364+
default: {
365+
...TEST_CONFIG.profiles.default,
366+
systemPrompt: "Database: ${DATABASE_URL}",
367+
},
368+
},
369+
},
370+
);
371+
372+
await lifecycle.createWorktree({
373+
branch: "feature/prompt-env",
374+
});
375+
376+
const worktreePath = join(repoRoot, "__worktrees", "feature", "prompt-env");
377+
const gitDir = new BunGitGateway().resolveWorktreeGitDir(worktreePath);
378+
const runtimeEnvText = await Bun.file(getWorktreeStoragePaths(gitDir).runtimeEnvPath).text();
379+
const agentCommand = tmux.commands.find(({ target }) =>
380+
target === `${buildProjectSessionName(repoRoot)}:${buildWorktreeWindowName("feature/prompt-env")}.0`
381+
)?.command;
382+
383+
expect(runtimeEnvText).toContain(databaseUrl);
384+
expect(agentCommand).toContain(`Database: ${databaseUrl}`);
385+
});
386+
343387
it("creates a managed worktree under an absolute worktree root", async () => {
344388
const repoRoot = await initRepo();
345389
const absoluteWorktreeRoot = await mkdtemp(join(tmpdir(), "webmux-absolute-worktrees-"));

backend/src/services/lifecycle-service.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ export class LifecycleService {
134134
worktreePath,
135135
});
136136

137+
initialized = await this.refreshManagedArtifactsFromMeta({
138+
gitDir: initialized.paths.gitDir,
139+
meta: initialized.meta,
140+
worktreePath,
141+
});
142+
137143
await this.materializeRuntimeSession({
138144
branch,
139145
profile,
@@ -374,23 +380,35 @@ export class LifecycleService {
374380
throw new Error("Missing managed metadata");
375381
}
376382

377-
const dotenvValues = await loadDotenvLocal(resolved.entry.path);
378-
const runtimeEnv = buildRuntimeEnvMap(resolved.meta, {
379-
WEBMUX_WORKTREE_PATH: resolved.entry.path,
383+
return await this.refreshManagedArtifactsFromMeta({
384+
gitDir: resolved.gitDir,
385+
meta: resolved.meta,
386+
worktreePath: resolved.entry.path,
387+
});
388+
}
389+
390+
private async refreshManagedArtifactsFromMeta(input: {
391+
gitDir: string;
392+
meta: WorktreeMeta;
393+
worktreePath: string;
394+
}): Promise<InitializeManagedWorktreeResult> {
395+
const dotenvValues = await loadDotenvLocal(input.worktreePath);
396+
const runtimeEnv = buildRuntimeEnvMap(input.meta, {
397+
WEBMUX_WORKTREE_PATH: input.worktreePath,
380398
}, dotenvValues);
381-
await writeRuntimeEnv(resolved.gitDir, runtimeEnv);
399+
await writeRuntimeEnv(input.gitDir, runtimeEnv);
382400

383401
const controlEnv = buildControlEnvMap({
384402
controlUrl: this.controlUrl(),
385403
controlToken: await this.deps.getControlToken(),
386-
worktreeId: resolved.meta.worktreeId,
387-
branch: resolved.meta.branch,
404+
worktreeId: input.meta.worktreeId,
405+
branch: input.meta.branch,
388406
});
389-
await writeControlEnv(resolved.gitDir, controlEnv);
407+
await writeControlEnv(input.gitDir, controlEnv);
390408

391409
return {
392-
meta: resolved.meta,
393-
paths: getWorktreeStoragePaths(resolved.gitDir),
410+
meta: input.meta,
411+
paths: getWorktreeStoragePaths(input.gitDir),
394412
runtimeEnv,
395413
controlEnv,
396414
};

0 commit comments

Comments
 (0)