Skip to content

Commit d95f628

Browse files
authored
fix(force-env): refresh managed files and rebuild on recreate (#19)
Co-authored-by: skulidropek <skulidropek@users.noreply.github.com>
1 parent df71239 commit d95f628

File tree

4 files changed

+126
-6
lines changed

4 files changed

+126
-6
lines changed

packages/lib/src/shell/docker.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,27 @@ export const runDockerComposeUp = (
6464
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
6565
runCompose(cwd, ["up", "-d", "--build"], [Number(ExitCode(0))])
6666

67-
// CHANGE: recreate running containers without rebuilding images
68-
// WHY: apply env-file changes while preserving workspace volumes and docker layer cache
67+
export const dockerComposeUpRecreateArgs: ReadonlyArray<string> = [
68+
"up",
69+
"-d",
70+
"--build",
71+
"--force-recreate"
72+
]
73+
74+
// CHANGE: recreate running containers and refresh images when needed
75+
// WHY: apply env/template updates while preserving workspace volumes
6976
// QUOTE(ТЗ): "сбросит только окружение"
7077
// REF: user-request-2026-02-11-force-env
7178
// SOURCE: n/a
72-
// FORMAT THEOREM: ∀dir: up_force_recreate(dir) → recreated(containers(dir)) ∧ preserved(volumes(dir))
79+
// FORMAT THEOREM: ∀dir: up_force_recreate(dir) → recreated(containers(dir)) ∧ preserved(volumes(dir)) ∧ updated(images(dir))
7380
// PURITY: SHELL
7481
// EFFECT: Effect<void, DockerCommandError | PlatformError, CommandExecutor>
75-
// INVARIANT: does not invoke image build and does not remove volumes
82+
// INVARIANT: may rebuild images but does not remove volumes
7683
// COMPLEXITY: O(command)
7784
export const runDockerComposeUpRecreate = (
7885
cwd: string
7986
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
80-
runCompose(cwd, ["up", "-d", "--force-recreate"], [Number(ExitCode(0))])
87+
runCompose(cwd, dockerComposeUpRecreateArgs, [Number(ExitCode(0))])
8188

8289
// CHANGE: run docker compose down in the target directory
8390
// WHY: allow stopping managed containers from the CLI/menu

packages/lib/src/usecases/actions/prepare-files.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,10 @@ export const prepareProjectFiles = (
122122
options: PrepareProjectFilesOptions
123123
): Effect.Effect<ReadonlyArray<string>, PrepareProjectFilesError, FileSystem.FileSystem | Path.Path> =>
124124
Effect.gen(function*(_) {
125+
const rewriteManagedFiles = options.force || options.forceEnv
125126
const envOnlyRefresh = options.forceEnv && !options.force
126127
const createdFiles = yield* _(
127-
writeProjectFiles(resolvedOutDir, projectConfig, options.force, envOnlyRefresh)
128+
writeProjectFiles(resolvedOutDir, projectConfig, rewriteManagedFiles)
128129
)
129130
yield* _(ensureAuthorizedKeys(resolvedOutDir, projectConfig.authorizedKeysPath))
130131
yield* _(ensureEnvFile(resolvedOutDir, projectConfig.envGlobalPath, defaultGlobalEnvContents))
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { describe, expect, it } from "@effect/vitest"
2+
3+
import { dockerComposeUpRecreateArgs } from "../../src/shell/docker.js"
4+
5+
describe("docker compose args", () => {
6+
it("uses build when force-env recreates containers", () => {
7+
expect(dockerComposeUpRecreateArgs).toEqual(["up", "-d", "--build", "--force-recreate"])
8+
})
9+
})
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import * as fs from "node:fs"
2+
import * as os from "node:os"
3+
import * as path from "node:path"
4+
5+
import { NodeContext } from "@effect/platform-node"
6+
import { describe, expect, it } from "@effect/vitest"
7+
import { Effect } from "effect"
8+
9+
import type { TemplateConfig } from "../../src/core/domain.js"
10+
import { prepareProjectFiles } from "../../src/usecases/actions/prepare-files.js"
11+
12+
const withTempDir = <A, E, R>(use: (tempDir: string) => Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
13+
Effect.scoped(
14+
Effect.gen(function*(_) {
15+
const tempDir = yield* _(
16+
Effect.acquireRelease(
17+
Effect.sync(() => fs.mkdtempSync(path.join(os.tmpdir(), "docker-git-force-env-"))),
18+
(dir) => Effect.sync(() => fs.rmSync(dir, { recursive: true, force: true }))
19+
)
20+
)
21+
return yield* _(use(tempDir))
22+
})
23+
)
24+
25+
const makeGlobalConfig = (root: string): TemplateConfig => ({
26+
containerName: "dg-test",
27+
serviceName: "dg-test",
28+
sshUser: "dev",
29+
sshPort: 2222,
30+
repoUrl: "https://github.com/org/repo.git",
31+
repoRef: "main",
32+
targetDir: "/home/dev/org/repo",
33+
volumeName: "dg-test-home",
34+
authorizedKeysPath: path.join(root, "authorized_keys"),
35+
envGlobalPath: path.join(root, ".orch/env/global.env"),
36+
envProjectPath: path.join(root, ".orch/env/project.env"),
37+
codexAuthPath: path.join(root, ".orch/auth/codex"),
38+
codexSharedAuthPath: path.join(root, ".orch/auth/codex-shared"),
39+
codexHome: "/home/dev/.codex",
40+
enableMcpPlaywright: false,
41+
pnpmVersion: "10.27.0"
42+
})
43+
44+
const makeProjectConfig = (outDir: string, enableMcpPlaywright: boolean): TemplateConfig => ({
45+
containerName: "dg-test",
46+
serviceName: "dg-test",
47+
sshUser: "dev",
48+
sshPort: 2222,
49+
repoUrl: "https://github.com/org/repo.git",
50+
repoRef: "main",
51+
targetDir: "/home/dev/org/repo",
52+
volumeName: "dg-test-home",
53+
authorizedKeysPath: path.join(outDir, "authorized_keys"),
54+
envGlobalPath: path.join(outDir, ".orch/env/global.env"),
55+
envProjectPath: path.join(outDir, ".orch/env/project.env"),
56+
codexAuthPath: path.join(outDir, ".orch/auth/codex"),
57+
codexSharedAuthPath: path.join(outDir, ".orch/auth/codex-shared"),
58+
codexHome: "/home/dev/.codex",
59+
enableMcpPlaywright,
60+
pnpmVersion: "10.27.0"
61+
})
62+
63+
describe("prepareProjectFiles", () => {
64+
it.effect("force-env refresh rewrites managed templates", () =>
65+
withTempDir((root) =>
66+
Effect.gen(function*(_) {
67+
const outDir = path.join(root, "project")
68+
const globalConfig = makeGlobalConfig(root)
69+
const withoutMcp = makeProjectConfig(outDir, false)
70+
const withMcp = makeProjectConfig(outDir, true)
71+
72+
yield* _(
73+
prepareProjectFiles(outDir, root, globalConfig, withoutMcp, {
74+
force: false,
75+
forceEnv: false
76+
})
77+
)
78+
79+
const composeBefore = yield* _(
80+
Effect.sync(() => fs.readFileSync(path.join(outDir, "docker-compose.yml"), "utf8"))
81+
)
82+
expect(composeBefore).not.toContain("dg-test-browser")
83+
84+
yield* _(
85+
prepareProjectFiles(outDir, root, globalConfig, withMcp, {
86+
force: false,
87+
forceEnv: true
88+
})
89+
)
90+
91+
const composeAfter = yield* _(
92+
Effect.sync(() => fs.readFileSync(path.join(outDir, "docker-compose.yml"), "utf8"))
93+
)
94+
const configAfter = yield* _(
95+
Effect.sync(() => JSON.parse(fs.readFileSync(path.join(outDir, "docker-git.json"), "utf8")))
96+
)
97+
98+
expect(composeAfter).toContain("dg-test-browser")
99+
expect(composeAfter).toContain('MCP_PLAYWRIGHT_ENABLE: "1"')
100+
expect(configAfter.template.enableMcpPlaywright).toBe(true)
101+
})
102+
).pipe(Effect.provide(NodeContext.layer)))
103+
})

0 commit comments

Comments
 (0)