Skip to content

Commit 5c5b20b

Browse files
committed
Merge branch 'main' into issue-123-a5ba76ddd9e9
2 parents 5a36587 + 2b906df commit 5c5b20b

File tree

8 files changed

+386
-5
lines changed

8 files changed

+386
-5
lines changed

packages/app/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @prover-coder-ai/docker-git
22

3+
## 1.0.35
4+
5+
### Patch Changes
6+
7+
- chore: automated version bump
8+
39
## 1.0.34
410

511
### Patch Changes

packages/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@prover-coder-ai/docker-git",
3-
"version": "1.0.34",
3+
"version": "1.0.35",
44
"description": "Minimal Vite-powered TypeScript console starter using Effect",
55
"main": "dist/src/docker-git/main.js",
66
"bin": {

packages/lib/src/usecases/auth-codex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type CodexAccountContext = {
2525

2626
const codexImageName = "docker-git-auth-codex:latest"
2727
const codexImageDir = ".docker-git/.orch/auth/codex/.image"
28-
const codexHome = "/root/.codex"
28+
const codexHome = "/codex-home"
2929

3030
const ensureCodexOrchLayout = (
3131
cwd: string,

packages/lib/src/usecases/auth-github.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const resolveGithubTokenFromGh = (
125125
image: ghImageName,
126126
hostPath: accountPath,
127127
containerPath: ghAuthDir,
128+
env: `GH_CONFIG_DIR=${ghAuthDir}`,
128129
args: ["auth", "token"],
129130
interactive: false
130131
}),
@@ -149,7 +150,7 @@ const runGithubLogin = (
149150
image: ghImageName,
150151
hostPath: accountPath,
151152
containerPath: ghAuthDir,
152-
env: "BROWSER=echo",
153+
env: ["BROWSER=echo", `GH_CONFIG_DIR=${ghAuthDir}`],
153154
args: [
154155
"auth",
155156
"login",

packages/lib/src/usecases/github-auth-image.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { CommandFailedError } from "../shell/errors.js"
88
import { ensureDockerImage } from "./docker-image.js"
99

1010
export const ghAuthRoot = ".docker-git/.orch/auth/gh"
11-
export const ghAuthDir = "/root/.config/gh"
11+
export const ghAuthDir = "/gh-auth"
1212
export const ghImageName = "docker-git-auth-gh:latest"
1313
export const ghImageDir = ".docker-git/.orch/auth/gh/.image"
1414

packages/lib/src/usecases/path-helpers.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,13 @@ export const findKeyByPriority = (
169169
}
170170
}
171171

172-
const home = resolveEnvPath("HOME")
172+
const dockerGitHomeKey = path.join(defaultProjectsRoot(cwd), spec.devKeyName)
173+
const dockerGitHomeExisting = yield* _(findExistingPath(fs, dockerGitHomeKey))
174+
if (dockerGitHomeExisting !== null) {
175+
return dockerGitHomeExisting
176+
}
177+
178+
const home = resolveHomeDir()
173179
if (home === null) {
174180
return null
175181
}
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import * as Command from "@effect/platform/Command"
2+
import * as CommandExecutor from "@effect/platform/CommandExecutor"
3+
import * as FileSystem from "@effect/platform/FileSystem"
4+
import { NodeContext } from "@effect/platform-node"
5+
import { describe, expect, it } from "@effect/vitest"
6+
import { Effect } from "effect"
7+
import * as Inspectable from "effect/Inspectable"
8+
import * as Sink from "effect/Sink"
9+
import * as Stream from "effect/Stream"
10+
11+
import { authCodexLogin } from "../../src/usecases/auth-codex.js"
12+
import { authGithubLogin } from "../../src/usecases/auth-github.js"
13+
14+
type RecordedCommand = {
15+
readonly command: string
16+
readonly args: ReadonlyArray<string>
17+
}
18+
19+
const encode = (value: string): Uint8Array => new TextEncoder().encode(value)
20+
21+
const withTempDir = <A, E, R>(
22+
use: (tempDir: string) => Effect.Effect<A, E, R>
23+
): Effect.Effect<A, E, R | FileSystem.FileSystem> =>
24+
Effect.scoped(
25+
Effect.gen(function*(_) {
26+
const fs = yield* _(FileSystem.FileSystem)
27+
const tempDir = yield* _(
28+
fs.makeTempDirectoryScoped({
29+
prefix: "docker-git-auth-paths-"
30+
})
31+
)
32+
return yield* _(use(tempDir))
33+
})
34+
)
35+
36+
const withPatchedEnv = <A, E, R>(
37+
patch: Readonly<Record<string, string | undefined>>,
38+
effect: Effect.Effect<A, E, R>
39+
): Effect.Effect<A, E, R> =>
40+
Effect.acquireUseRelease(
41+
Effect.sync(() => {
42+
const previous = new Map<string, string | undefined>()
43+
for (const [key, value] of Object.entries(patch)) {
44+
previous.set(key, process.env[key])
45+
if (value === undefined) {
46+
delete process.env[key]
47+
} else {
48+
process.env[key] = value
49+
}
50+
}
51+
return previous
52+
}),
53+
() => effect,
54+
(previous) =>
55+
Effect.sync(() => {
56+
for (const [key, value] of previous.entries()) {
57+
if (value === undefined) {
58+
delete process.env[key]
59+
} else {
60+
process.env[key] = value
61+
}
62+
}
63+
})
64+
)
65+
66+
const withWorkingDirectory = <A, E, R>(
67+
cwd: string,
68+
effect: Effect.Effect<A, E, R>
69+
): Effect.Effect<A, E, R> =>
70+
Effect.acquireUseRelease(
71+
Effect.sync(() => {
72+
const previous = process.cwd()
73+
process.chdir(cwd)
74+
return previous
75+
}),
76+
() => effect,
77+
(previous) =>
78+
Effect.sync(() => {
79+
process.chdir(previous)
80+
})
81+
)
82+
83+
const includesArgsInOrder = (
84+
args: ReadonlyArray<string>,
85+
expectedSequence: ReadonlyArray<string>
86+
): boolean => {
87+
let searchFrom = 0
88+
for (const expected of expectedSequence) {
89+
const foundAt = args.indexOf(expected, searchFrom)
90+
if (foundAt === -1) {
91+
return false
92+
}
93+
searchFrom = foundAt + 1
94+
}
95+
return true
96+
}
97+
98+
const isDockerRunFor = (
99+
entry: RecordedCommand,
100+
image: string,
101+
args: ReadonlyArray<string>
102+
): boolean =>
103+
entry.command === "docker" &&
104+
includesArgsInOrder(entry.args, ["run", "--rm"]) &&
105+
includesArgsInOrder(entry.args, [image, ...args])
106+
107+
const makeFakeExecutor = (
108+
recorded: Array<RecordedCommand>
109+
): CommandExecutor.CommandExecutor => {
110+
const start = (command: Command.Command): Effect.Effect<CommandExecutor.Process, never> =>
111+
Effect.gen(function*(_) {
112+
const flattened = Command.flatten(command)
113+
for (const entry of flattened) {
114+
recorded.push({ command: entry.command, args: entry.args })
115+
}
116+
117+
const last = flattened[flattened.length - 1]!
118+
const invocation: RecordedCommand = { command: last.command, args: last.args }
119+
const stdoutText = isDockerRunFor(invocation, "docker-git-auth-gh:latest", ["auth", "token"])
120+
? "test-gh-token\n"
121+
: ""
122+
const stdout = stdoutText.length === 0 ? Stream.empty : Stream.succeed(encode(stdoutText))
123+
124+
const process: CommandExecutor.Process = {
125+
[CommandExecutor.ProcessTypeId]: CommandExecutor.ProcessTypeId,
126+
pid: CommandExecutor.ProcessId(1),
127+
exitCode: Effect.succeed(CommandExecutor.ExitCode(0)),
128+
isRunning: Effect.succeed(false),
129+
kill: (_signal) => Effect.void,
130+
stderr: Stream.empty,
131+
stdin: Sink.drain,
132+
stdout,
133+
toJSON: () => ({ _tag: "AuthContainerPathsTestProcess", command: invocation.command, args: invocation.args }),
134+
[Inspectable.NodeInspectSymbol]: () => ({
135+
_tag: "AuthContainerPathsTestProcess",
136+
command: invocation.command,
137+
args: invocation.args
138+
}),
139+
toString: () => `[AuthContainerPathsTestProcess ${invocation.command}]`
140+
}
141+
142+
return process
143+
})
144+
145+
return CommandExecutor.makeExecutor(start)
146+
}
147+
148+
describe("auth container paths", () => {
149+
it.effect("pins gh auth login and token reads to the same writable config dir", () =>
150+
withTempDir((root) =>
151+
Effect.gen(function*(_) {
152+
const fs = yield* _(FileSystem.FileSystem)
153+
const envPath = `${root}/.docker-git/.orch/env/global.env`
154+
const accountPath = `${root}/.docker-git/.orch/auth/gh/default`
155+
const recorded: Array<RecordedCommand> = []
156+
const executor = makeFakeExecutor(recorded)
157+
158+
yield* _(
159+
withPatchedEnv(
160+
{
161+
HOME: root,
162+
DOCKER_GIT_STATE_AUTO_SYNC: "0"
163+
},
164+
withWorkingDirectory(
165+
root,
166+
authGithubLogin({
167+
_tag: "AuthGithubLogin",
168+
label: null,
169+
token: null,
170+
scopes: null,
171+
envGlobalPath: ".docker-git/.orch/env/global.env"
172+
}).pipe(Effect.provideService(CommandExecutor.CommandExecutor, executor))
173+
)
174+
)
175+
)
176+
177+
const loginCommand = recorded.find((entry) =>
178+
isDockerRunFor(entry, "docker-git-auth-gh:latest", ["auth", "login"])
179+
)
180+
const tokenCommand = recorded.find((entry) =>
181+
isDockerRunFor(entry, "docker-git-auth-gh:latest", ["auth", "token"])
182+
)
183+
184+
expect(loginCommand).toBeDefined()
185+
expect(tokenCommand).toBeDefined()
186+
expect(
187+
includesArgsInOrder(loginCommand?.args ?? [], [
188+
"-v",
189+
`${accountPath}:/gh-auth`,
190+
"-e",
191+
"BROWSER=echo",
192+
"-e",
193+
"GH_CONFIG_DIR=/gh-auth",
194+
"docker-git-auth-gh:latest",
195+
"auth",
196+
"login"
197+
])
198+
).toBe(true)
199+
expect(
200+
includesArgsInOrder(tokenCommand?.args ?? [], [
201+
"-v",
202+
`${accountPath}:/gh-auth`,
203+
"-e",
204+
"GH_CONFIG_DIR=/gh-auth",
205+
"docker-git-auth-gh:latest",
206+
"auth",
207+
"token"
208+
])
209+
).toBe(true)
210+
211+
const envText = yield* _(fs.readFileString(envPath))
212+
expect(envText).toContain("GITHUB_TOKEN=test-gh-token")
213+
})
214+
).pipe(Effect.provide(NodeContext.layer)))
215+
216+
it.effect("runs codex auth against a non-root CODEX_HOME mount", () =>
217+
withTempDir((root) =>
218+
Effect.gen(function*(_) {
219+
const recorded: Array<RecordedCommand> = []
220+
const executor = makeFakeExecutor(recorded)
221+
222+
yield* _(
223+
withPatchedEnv(
224+
{
225+
HOME: root,
226+
DOCKER_GIT_STATE_AUTO_SYNC: "0"
227+
},
228+
withWorkingDirectory(
229+
root,
230+
authCodexLogin({
231+
_tag: "AuthCodexLogin",
232+
label: null,
233+
codexAuthPath: ".docker-git/.orch/auth/codex"
234+
}).pipe(Effect.provideService(CommandExecutor.CommandExecutor, executor))
235+
)
236+
)
237+
)
238+
239+
const loginCommand = recorded.find((entry) =>
240+
isDockerRunFor(entry, "docker-git-auth-codex:latest", ["codex", "login", "--device-auth"])
241+
)
242+
243+
expect(loginCommand).toBeDefined()
244+
expect(loginCommand?.args.some((arg) => arg.endsWith(":/codex-home")) ?? false).toBe(true)
245+
expect(loginCommand?.args.includes("CODEX_HOME=/codex-home") ?? false).toBe(true)
246+
})
247+
).pipe(Effect.provide(NodeContext.layer)))
248+
})

0 commit comments

Comments
 (0)