Skip to content

Commit 034d133

Browse files
authored
fix: support nested docker-git auth bootstrap and compose v2 (#25)
* fix(lib): bootstrap nested docker-git auth and compose v2 * refactor(lib): split nested docker-git entrypoint template * refactor(lib): reduce lint complexity in path normalization
1 parent 0612c56 commit 034d133

File tree

11 files changed

+188
-31
lines changed

11 files changed

+188
-31
lines changed

packages/lib/src/core/command-builders.ts

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ const resolveNames = (
104104
})
105105

106106
type PathConfig = {
107+
readonly dockerGitPath: string
107108
readonly authorizedKeysPath: string
108109
readonly envGlobalPath: string
109110
readonly envProjectPath: string
@@ -113,43 +114,72 @@ type PathConfig = {
113114
readonly outDir: string
114115
}
115116

117+
type DefaultPathConfig = {
118+
readonly dockerGitPath: string
119+
readonly authorizedKeysPath: string
120+
readonly envGlobalPath: string
121+
readonly envProjectPath: string
122+
readonly codexAuthPath: string
123+
}
124+
125+
const resolveNormalizedSecretsRoot = (value: string | undefined): string | undefined => {
126+
const trimmed = value?.trim() ?? ""
127+
return trimmed.length === 0 ? undefined : normalizeSecretsRoot(trimmed)
128+
}
129+
130+
const buildDefaultPathConfig = (
131+
normalizedSecretsRoot: string | undefined,
132+
projectSlug: string
133+
): DefaultPathConfig =>
134+
normalizedSecretsRoot === undefined
135+
? {
136+
dockerGitPath: defaultTemplateConfig.dockerGitPath,
137+
authorizedKeysPath: defaultTemplateConfig.authorizedKeysPath,
138+
envGlobalPath: defaultTemplateConfig.envGlobalPath,
139+
envProjectPath: defaultTemplateConfig.envProjectPath,
140+
codexAuthPath: defaultTemplateConfig.codexAuthPath
141+
}
142+
: {
143+
dockerGitPath: normalizedSecretsRoot,
144+
authorizedKeysPath: `${normalizedSecretsRoot}/authorized_keys`,
145+
envGlobalPath: `${normalizedSecretsRoot}/global.env`,
146+
envProjectPath: `${normalizedSecretsRoot}/${projectSlug}.env`,
147+
codexAuthPath: `${normalizedSecretsRoot}/codex`
148+
}
149+
116150
const resolvePaths = (
117151
raw: RawOptions,
118152
projectSlug: string,
119153
repoPath: string
120154
): Either.Either<PathConfig, ParseError> =>
121155
Either.gen(function*(_) {
122-
const secretsRoot = raw.secretsRoot?.trim()
123-
const normalizedSecretsRoot = secretsRoot === undefined || secretsRoot.length === 0
124-
? undefined
125-
: normalizeSecretsRoot(secretsRoot)
126-
const defaultAuthorizedKeysPath = normalizedSecretsRoot === undefined
127-
? defaultTemplateConfig.authorizedKeysPath
128-
: `${normalizedSecretsRoot}/authorized_keys`
129-
const defaultEnvGlobalPath = normalizedSecretsRoot === undefined
130-
? defaultTemplateConfig.envGlobalPath
131-
: `${normalizedSecretsRoot}/global.env`
132-
const defaultEnvProjectPath = normalizedSecretsRoot === undefined
133-
? defaultTemplateConfig.envProjectPath
134-
: `${normalizedSecretsRoot}/${projectSlug}.env`
135-
const defaultCodexAuthPath = normalizedSecretsRoot === undefined
136-
? defaultTemplateConfig.codexAuthPath
137-
: `${normalizedSecretsRoot}/codex`
156+
const normalizedSecretsRoot = resolveNormalizedSecretsRoot(raw.secretsRoot)
157+
const defaults = buildDefaultPathConfig(normalizedSecretsRoot, projectSlug)
158+
const dockerGitPath = defaults.dockerGitPath
138159
const authorizedKeysPath = yield* _(
139-
nonEmpty("--authorized-keys", raw.authorizedKeysPath, defaultAuthorizedKeysPath)
160+
nonEmpty("--authorized-keys", raw.authorizedKeysPath, defaults.authorizedKeysPath)
140161
)
141-
const envGlobalPath = yield* _(nonEmpty("--env-global", raw.envGlobalPath, defaultEnvGlobalPath))
162+
const envGlobalPath = yield* _(nonEmpty("--env-global", raw.envGlobalPath, defaults.envGlobalPath))
142163
const envProjectPath = yield* _(
143-
nonEmpty("--env-project", raw.envProjectPath, defaultEnvProjectPath)
164+
nonEmpty("--env-project", raw.envProjectPath, defaults.envProjectPath)
144165
)
145166
const codexAuthPath = yield* _(
146-
nonEmpty("--codex-auth", raw.codexAuthPath, defaultCodexAuthPath)
167+
nonEmpty("--codex-auth", raw.codexAuthPath, defaults.codexAuthPath)
147168
)
148169
const codexSharedAuthPath = codexAuthPath
149170
const codexHome = yield* _(nonEmpty("--codex-home", raw.codexHome, defaultTemplateConfig.codexHome))
150171
const outDir = yield* _(nonEmpty("--out-dir", raw.outDir, `.docker-git/${repoPath}`))
151172

152-
return { authorizedKeysPath, envGlobalPath, envProjectPath, codexAuthPath, codexSharedAuthPath, codexHome, outDir }
173+
return {
174+
dockerGitPath,
175+
authorizedKeysPath,
176+
envGlobalPath,
177+
envProjectPath,
178+
codexAuthPath,
179+
codexSharedAuthPath,
180+
codexHome,
181+
outDir
182+
}
153183
})
154184

155185
// CHANGE: build a typed create command from raw options (CLI or API)
@@ -190,6 +220,7 @@ export const buildCreateCommand = (
190220
repoRef: repo.repoRef,
191221
targetDir: repo.targetDir,
192222
volumeName: names.volumeName,
223+
dockerGitPath: paths.dockerGitPath,
193224
authorizedKeysPath: paths.authorizedKeysPath,
194225
envGlobalPath: paths.envGlobalPath,
195226
envProjectPath: paths.envProjectPath,

packages/lib/src/core/domain.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface TemplateConfig {
1212
readonly forkRepoUrl?: string
1313
readonly targetDir: string
1414
readonly volumeName: string
15+
readonly dockerGitPath: string
1516
readonly authorizedKeysPath: string
1617
readonly envGlobalPath: string
1718
readonly envProjectPath: string
@@ -194,6 +195,7 @@ export const defaultTemplateConfig = {
194195
repoRef: "main",
195196
targetDir: "/home/dev/app",
196197
volumeName: "dev_home",
198+
dockerGitPath: "./.docker-git",
197199
authorizedKeysPath: "./.docker-git/authorized_keys",
198200
envGlobalPath: "./.docker-git/.orch/env/global.env",
199201
envProjectPath: "./.orch/env/project.env",

packages/lib/src/core/templates-entrypoint.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
renderEntrypointMcpPlaywright
1919
} from "./templates-entrypoint/codex.js"
2020
import { renderEntrypointGitConfig, renderEntrypointGitHooks } from "./templates-entrypoint/git.js"
21+
import { renderEntrypointDockerGitBootstrap } from "./templates-entrypoint/nested-docker-git.js"
2122
import { renderEntrypointBackgroundTasks } from "./templates-entrypoint/tasks.js"
2223
import {
2324
renderEntrypointBashCompletion,
@@ -32,6 +33,7 @@ export const renderEntrypoint = (config: TemplateConfig): string =>
3233
renderEntrypointAuthorizedKeys(config),
3334
renderEntrypointCodexHome(config),
3435
renderEntrypointCodexSharedAuth(config),
36+
renderEntrypointDockerGitBootstrap(config),
3537
renderEntrypointMcpPlaywright(config),
3638
renderEntrypointZshShell(config),
3739
renderEntrypointZshUserRc(config),
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { TemplateConfig } from "../domain.js"
2+
3+
const entrypointDockerGitBootstrapTemplate = String.raw`# Bootstrap ~/.docker-git for nested docker-git usage inside this container.
4+
DOCKER_GIT_HOME="/home/__SSH_USER__/.docker-git"
5+
DOCKER_GIT_AUTH_DIR="$DOCKER_GIT_HOME/.orch/auth/codex"
6+
DOCKER_GIT_ENV_DIR="$DOCKER_GIT_HOME/.orch/env"
7+
DOCKER_GIT_ENV_GLOBAL="$DOCKER_GIT_ENV_DIR/global.env"
8+
DOCKER_GIT_ENV_PROJECT="$DOCKER_GIT_ENV_DIR/project.env"
9+
DOCKER_GIT_AUTH_KEYS="$DOCKER_GIT_HOME/authorized_keys"
10+
11+
mkdir -p "$DOCKER_GIT_AUTH_DIR" "$DOCKER_GIT_ENV_DIR" "$DOCKER_GIT_HOME/.orch/auth/gh"
12+
13+
if [[ -f "/home/__SSH_USER__/.ssh/authorized_keys" ]]; then
14+
cp "/home/__SSH_USER__/.ssh/authorized_keys" "$DOCKER_GIT_AUTH_KEYS"
15+
elif [[ -f /authorized_keys ]]; then
16+
cp /authorized_keys "$DOCKER_GIT_AUTH_KEYS"
17+
fi
18+
if [[ -f "$DOCKER_GIT_AUTH_KEYS" ]]; then
19+
chmod 600 "$DOCKER_GIT_AUTH_KEYS" || true
20+
fi
21+
22+
if [[ ! -f "$DOCKER_GIT_ENV_GLOBAL" ]]; then
23+
cat <<'EOF' > "$DOCKER_GIT_ENV_GLOBAL"
24+
# docker-git env
25+
# KEY=value
26+
EOF
27+
fi
28+
if [[ ! -f "$DOCKER_GIT_ENV_PROJECT" ]]; then
29+
cat <<'EOF' > "$DOCKER_GIT_ENV_PROJECT"
30+
# docker-git project env defaults
31+
CODEX_SHARE_AUTH=1
32+
CODEX_AUTO_UPDATE=1
33+
DOCKER_GIT_ZSH_AUTOSUGGEST=1
34+
DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=fg=8,italic
35+
DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=history completion
36+
MCP_PLAYWRIGHT_ISOLATED=1
37+
EOF
38+
fi
39+
40+
upsert_env_var() {
41+
local file="$1"
42+
local key="$2"
43+
local value="$3"
44+
local tmp
45+
tmp="$(mktemp)"
46+
awk -v key="$key" 'index($0, key "=") != 1 { print }' "$file" > "$tmp"
47+
printf "%s=%s\n" "$key" "$value" >> "$tmp"
48+
mv "$tmp" "$file"
49+
}
50+
51+
copy_if_distinct_file() {
52+
local source="$1"
53+
local target="$2"
54+
if [[ ! -f "$source" ]]; then
55+
return 1
56+
fi
57+
local source_real=""
58+
local target_real=""
59+
source_real="$(readlink -f "$source" 2>/dev/null || true)"
60+
target_real="$(readlink -f "$target" 2>/dev/null || true)"
61+
if [[ -n "$source_real" && -n "$target_real" && "$source_real" == "$target_real" ]]; then
62+
return 0
63+
fi
64+
cp "$source" "$target"
65+
return 0
66+
}
67+
68+
if [[ -n "$GH_TOKEN" ]]; then
69+
upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GH_TOKEN" "$GH_TOKEN"
70+
fi
71+
if [[ -n "$GITHUB_TOKEN" ]]; then
72+
upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GITHUB_TOKEN" "$GITHUB_TOKEN"
73+
elif [[ -n "$GH_TOKEN" ]]; then
74+
upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GITHUB_TOKEN" "$GH_TOKEN"
75+
fi
76+
77+
SOURCE_CODEX_CONFIG="__CODEX_HOME__/config.toml"
78+
copy_if_distinct_file "$SOURCE_CODEX_CONFIG" "$DOCKER_GIT_AUTH_DIR/config.toml" || true
79+
80+
SOURCE_SHARED_AUTH="__CODEX_HOME__-shared/auth.json"
81+
SOURCE_LOCAL_AUTH="__CODEX_HOME__/auth.json"
82+
if [[ -f "$SOURCE_SHARED_AUTH" ]]; then
83+
copy_if_distinct_file "$SOURCE_SHARED_AUTH" "$DOCKER_GIT_AUTH_DIR/auth.json" || true
84+
elif [[ -f "$SOURCE_LOCAL_AUTH" ]]; then
85+
copy_if_distinct_file "$SOURCE_LOCAL_AUTH" "$DOCKER_GIT_AUTH_DIR/auth.json" || true
86+
fi
87+
if [[ -f "$DOCKER_GIT_AUTH_DIR/auth.json" ]]; then
88+
chmod 600 "$DOCKER_GIT_AUTH_DIR/auth.json" || true
89+
fi
90+
91+
chown -R 1000:1000 "$DOCKER_GIT_HOME" || true`
92+
93+
export const renderEntrypointDockerGitBootstrap = (config: TemplateConfig): string =>
94+
entrypointDockerGitBootstrapTemplate
95+
.replaceAll("__SSH_USER__", config.sshUser)
96+
.replaceAll("__CODEX_HOME__", config.codexHome)

packages/lib/src/core/templates/docker-compose.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ ${maybePlaywrightEnv}${maybeDependsOn} env_file:
3838
- "127.0.0.1:${config.sshPort}:22"
3939
volumes:
4040
- ${config.volumeName}:/home/${config.sshUser}
41+
- ${config.dockerGitPath}:/home/${config.sshUser}/.docker-git
4142
- ${config.authorizedKeysPath}:/authorized_keys:ro
4243
- ${config.codexAuthPath}:${config.codexHome}
4344
- ${config.codexSharedAuthPath}:${config.codexHome}-shared

packages/lib/src/core/templates/dockerfile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ ENV NVM_DIR=/usr/local/nvm
99
1010
RUN apt-get update && apt-get install -y --no-install-recommends \
1111
openssh-server git gh ca-certificates curl unzip bsdutils sudo \
12-
make docker.io docker-compose bash-completion zsh zsh-autosuggestions xauth \
12+
make docker.io docker-compose-v2 bash-completion zsh zsh-autosuggestions xauth \
1313
ncurses-term \
1414
&& rm -rf /var/lib/apt/lists/*
1515

packages/lib/src/shell/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const TemplateConfigSchema = Schema.Struct({
1919
repoRef: Schema.String,
2020
targetDir: Schema.String,
2121
volumeName: Schema.String,
22+
dockerGitPath: Schema.optionalWith(Schema.String, {
23+
default: () => defaultTemplateConfig.dockerGitPath
24+
}),
2225
authorizedKeysPath: Schema.String,
2326
envGlobalPath: Schema.optionalWith(Schema.String, {
2427
default: () => defaultTemplateConfig.envGlobalPath

packages/lib/src/usecases/actions/create-project.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const makeCreateContext = (path: Path.Path, baseDir: string): CreateContext => {
4646

4747
const resolveRootedConfig = (command: CreateCommand, ctx: CreateContext): CreateCommand["config"] => ({
4848
...command.config,
49+
dockerGitPath: ctx.resolveRootPath(command.config.dockerGitPath),
4950
authorizedKeysPath: ctx.resolveRootPath(command.config.authorizedKeysPath),
5051
envGlobalPath: ctx.resolveRootPath(command.config.envGlobalPath),
5152
envProjectPath: ctx.resolveRootPath(command.config.envProjectPath),

packages/lib/src/usecases/actions/paths.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const buildProjectConfigs = (
4444

4545
const globalConfig = {
4646
...resolvedConfig,
47+
dockerGitPath: resolvePathFromBase(path, baseDir, resolvedConfig.dockerGitPath),
4748
authorizedKeysPath: resolvePathFromBase(path, baseDir, resolvedConfig.authorizedKeysPath),
4849
envGlobalPath: resolvePathFromBase(path, baseDir, resolvedConfig.envGlobalPath),
4950
envProjectPath: resolvePathFromBase(path, baseDir, resolvedConfig.envProjectPath),
@@ -52,6 +53,7 @@ export const buildProjectConfigs = (
5253
}
5354
const projectConfig = {
5455
...resolvedConfig,
56+
dockerGitPath: relativeFromOutDir(globalConfig.dockerGitPath),
5557
authorizedKeysPath: relativeFromOutDir(globalConfig.authorizedKeysPath),
5658
envGlobalPath: "./.orch/env/global.env",
5759
envProjectPath: path.isAbsolute(resolvedConfig.envProjectPath)

packages/lib/src/usecases/state-normalize.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,35 @@ const isLegacyDockerGitRelativePath = (value: string): boolean => {
2121
const shouldNormalizePath = (path: Path.Path, value: string): boolean =>
2222
path.isAbsolute(value) || isLegacyDockerGitRelativePath(value)
2323

24+
const withFallback = (value: string, fallback: string): string =>
25+
value.length > 0 ? value : fallback
26+
27+
const pathFieldsForNormalization = (template: TemplateConfig): ReadonlyArray<string> => [
28+
template.dockerGitPath,
29+
template.authorizedKeysPath,
30+
template.envGlobalPath,
31+
template.envProjectPath,
32+
template.codexAuthPath,
33+
template.codexSharedAuthPath
34+
]
35+
36+
const hasLegacyTemplatePaths = (path: Path.Path, template: TemplateConfig): boolean =>
37+
pathFieldsForNormalization(template).some((value) => shouldNormalizePath(path, value))
38+
2439
const normalizeTemplateConfig = (
2540
path: Path.Path,
2641
projectsRoot: string,
2742
projectDir: string,
2843
template: TemplateConfig
2944
): TemplateConfig | null => {
30-
const needs = shouldNormalizePath(path, template.authorizedKeysPath) ||
31-
shouldNormalizePath(path, template.envGlobalPath) ||
32-
shouldNormalizePath(path, template.envProjectPath) ||
33-
shouldNormalizePath(path, template.codexAuthPath) ||
34-
shouldNormalizePath(path, template.codexSharedAuthPath)
35-
36-
if (!needs) {
45+
if (!hasLegacyTemplatePaths(path, template)) {
3746
return null
3847
}
3948

4049
// The state repo is shared across machines, so never persist absolute host paths in tracked files.
4150
const authorizedKeysAbs = path.join(projectsRoot, "authorized_keys")
4251
const authorizedKeysRel = toPosixPath(path.relative(projectDir, authorizedKeysAbs))
52+
const dockerGitRel = toPosixPath(path.relative(projectDir, projectsRoot))
4353

4454
const envGlobalPath = "./.orch/env/global.env"
4555
const envProjectPath = "./.orch/env/project.env"
@@ -49,11 +59,12 @@ const normalizeTemplateConfig = (
4959

5060
return {
5161
...template,
52-
authorizedKeysPath: authorizedKeysRel.length > 0 ? authorizedKeysRel : "./authorized_keys",
62+
dockerGitPath: withFallback(dockerGitRel, "./.docker-git"),
63+
authorizedKeysPath: withFallback(authorizedKeysRel, "./authorized_keys"),
5364
envGlobalPath,
5465
envProjectPath,
5566
codexAuthPath,
56-
codexSharedAuthPath: codexSharedRel.length > 0 ? codexSharedRel : "./.orch/auth/codex"
67+
codexSharedAuthPath: withFallback(codexSharedRel, "./.orch/auth/codex")
5768
}
5869
}
5970

0 commit comments

Comments
 (0)