Skip to content

Commit 712cec1

Browse files
committed
fix(ci): restore bootstrap and e2e flows
1 parent 00ffe39 commit 712cec1

File tree

9 files changed

+157
-71
lines changed

9 files changed

+157
-71
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,5 @@ PrintLastLog no
163163
EOF
164164
chmod 0644 "$DOCKER_GIT_SSHD_CONF" || true`
165165

166-
export const renderEntrypointSshd = (): string => `# 5) Run sshd in foreground\nexec /usr/sbin/sshd -D`
166+
export const renderEntrypointSshd = (): string =>
167+
`# 5) Run sshd in foreground (log to stderr for CI/debuggability)\nexec /usr/sbin/sshd -D -e`

packages/lib/src/core/templates-entrypoint/nested-docker-git.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ docker_git_export_env_if_unset() {
136136
137137
if [[ -n "${"$"}{!key+x}" ]]; then
138138
docker_git_upsert_ssh_env "$key" "${"$"}{!key}"
139-
return 1
139+
return 0
140140
fi
141141
142142
export "$key=$value"

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ const renderResourceLimits = (resourceLimits: ResolvedComposeResourceLimits | un
6262
? ""
6363
: ` cpus: ${resourceLimits.cpuLimit}\n mem_limit: "${resourceLimits.ramLimit}"\n memswap_limit: "${resourceLimits.ramLimit}"\n`
6464

65-
const renderBootstrapMounts = (): string =>
66-
` - ${bootstrapVolumeKey}:/opt/docker-git/bootstrap/source:ro`
65+
const renderBootstrapMounts = (): string => ` - ${bootstrapVolumeKey}:/opt/docker-git/bootstrap/source:ro`
6766

6867
const buildPlaywrightFragments = (
6968
config: TemplateConfig,

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,31 @@ RUN claude --version`
6565
const renderDockerfileOpenCode = (): string =>
6666
`# Tooling: OpenCode (binary)
6767
RUN set -eu; \
68+
ARCH="$(uname -m)"; \
69+
case "$ARCH" in \
70+
x86_64|amd64) OPENCODE_ARCH="x64" ;; \
71+
aarch64|arm64) OPENCODE_ARCH="arm64" ;; \
72+
*) echo "Unsupported arch for OpenCode: $ARCH" >&2; exit 1 ;; \
73+
esac; \
74+
OPENCODE_TARGET="linux-$OPENCODE_ARCH"; \
75+
if [ "$OPENCODE_ARCH" = "x64" ] && ! grep -qwi avx2 /proc/cpuinfo 2>/dev/null; then \
76+
OPENCODE_TARGET="$OPENCODE_TARGET-baseline"; \
77+
fi; \
78+
if [ -f /etc/alpine-release ] || { command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; }; then \
79+
OPENCODE_TARGET="$OPENCODE_TARGET-musl"; \
80+
fi; \
81+
OPENCODE_ARCHIVE="opencode-$OPENCODE_TARGET.tar.gz"; \
82+
mkdir -p /usr/local/.opencode/bin; \
6883
for attempt in 1 2 3 4 5; do \
69-
if curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 https://opencode.ai/install \
70-
| HOME=/usr/local bash -s -- --no-modify-path; then \
84+
tmp_archive="$(mktemp)"; \
85+
if curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 \
86+
"https://github.com/anomalyco/opencode/releases/latest/download/$OPENCODE_ARCHIVE" \
87+
-o "$tmp_archive" \
88+
&& tar -xzf "$tmp_archive" -C /usr/local/.opencode/bin opencode; then \
89+
rm -f "$tmp_archive"; \
7190
exit 0; \
7291
fi; \
92+
rm -f "$tmp_archive"; \
7393
echo "opencode install attempt \${attempt} failed; retrying..." >&2; \
7494
sleep $((attempt * 2)); \
7595
done; \

packages/lib/src/shell/docker-volume.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import type { Effect } from "effect"
66
import { runCommandWithExitCodes } from "./command-runner.js"
77
import { DockerCommandError } from "./errors.js"
88

9-
const shellEscape = (value: string): string => `'${value.replaceAll("'", "'\\''")}'`
9+
const escapedSingleQuote = String.raw`'\''`
10+
11+
const shellEscape = (value: string): string => `'${value.replaceAll("'", escapedSingleQuote)}'`
1012

1113
export const runDockerVolumeCreate = (
1214
cwd: string,
@@ -31,13 +33,16 @@ export const runDockerVolumeReplaceFromDirectory = (
3133
volumeName: string,
3234
sourceDir: string
3335
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> => {
34-
const command =
35-
`tar -C ${shellEscape(sourceDir)} -cf - . | ` +
36-
`docker run --rm -i -v ${shellEscape(`${volumeName}:/target`)} alpine:3.20 ` +
37-
`sh -euc ${shellEscape("mkdir -p /target && find /target -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + && tar -xf - -C /target")}`
36+
const targetVolume = `${volumeName}:/target`
37+
const replaceCommand = shellEscape(
38+
"mkdir -p /target && find /target -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + && tar -xf - -C /target"
39+
)
40+
const command = `tar -C ${shellEscape(sourceDir)} -cf - . | ` +
41+
`docker run --rm -i -v ${shellEscape(targetVolume)} alpine:3.20 ` +
42+
`sh -euc ${replaceCommand}`
3843

3944
return runCommandWithExitCodes(
40-
{ cwd, command: "bash", args: ["-lc", command] },
45+
{ cwd, command: "bash", args: ["-c", command] },
4146
[Number(ExitCode(0))],
4247
(exitCode) => new DockerCommandError({ exitCode })
4348
)

packages/lib/src/usecases/shared-volume-seed.ts

Lines changed: 104 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ import {
1010
resolveProjectBootstrapVolumeName,
1111
type TemplateConfig
1212
} from "../core/domain.js"
13-
import {
14-
runDockerVolumeCreate,
15-
runDockerVolumeReplaceFromDirectory
16-
} from "../shell/docker-volume.js"
13+
import { runDockerVolumeCreate, runDockerVolumeReplaceFromDirectory } from "../shell/docker-volume.js"
1714
import type { DockerCommandError } from "../shell/errors.js"
1815

1916
type SharedVolumeSeedEnvironment = CommandExecutor | FileSystem.FileSystem | Path.Path
@@ -74,63 +71,117 @@ const copyFileIfPresent = (
7471
yield* _(fs.copyFile(sourcePath, targetPath))
7572
})
7673

74+
type BootstrapSeedConfig = Pick<
75+
TemplateConfig,
76+
"authorizedKeysPath" | "envGlobalPath" | "envProjectPath" | "codexAuthPath" | "codexSharedAuthPath"
77+
>
78+
79+
type BootstrapSnapshotSources = {
80+
readonly authorizedKeysSource: string
81+
readonly envGlobalSource: string
82+
readonly envProjectSource: string
83+
readonly codexAuthSource: string
84+
readonly codexSharedAuthSource: string
85+
readonly claudeAuthSource: string
86+
}
87+
88+
type BootstrapSnapshotTargets = {
89+
readonly authorizedKeysTarget: string
90+
readonly envGlobalTarget: string
91+
readonly envProjectTarget: string
92+
readonly projectCodexTarget: string
93+
readonly projectClaudeTarget: string
94+
readonly sharedCodexTarget: string
95+
}
96+
97+
const resolveBootstrapSnapshotSources = (
98+
path: Path.Path,
99+
projectDir: string,
100+
config: BootstrapSeedConfig
101+
): BootstrapSnapshotSources => {
102+
const codexAuthSource = resolvePathFromBase(path, projectDir, config.codexAuthPath)
103+
return {
104+
authorizedKeysSource: resolvePathFromBase(path, projectDir, config.authorizedKeysPath),
105+
envGlobalSource: resolvePathFromBase(path, projectDir, config.envGlobalPath),
106+
envProjectSource: resolvePathFromBase(path, projectDir, config.envProjectPath),
107+
codexAuthSource,
108+
codexSharedAuthSource: resolvePathFromBase(path, projectDir, config.codexSharedAuthPath),
109+
claudeAuthSource: path.join(path.dirname(codexAuthSource), "claude")
110+
}
111+
}
112+
113+
const resolveBootstrapSnapshotTargets = (
114+
path: Path.Path,
115+
stagingDir: string,
116+
config: BootstrapSeedConfig
117+
): BootstrapSnapshotTargets => {
118+
const authorizedKeysBase = config.authorizedKeysPath.replaceAll("\\", "/").split("/").at(-1) ?? "authorized_keys"
119+
const envGlobalBase = config.envGlobalPath.replaceAll("\\", "/").split("/").at(-1) ?? "global.env"
120+
const envProjectBase = config.envProjectPath.replaceAll("\\", "/").split("/").at(-1) ?? "project.env"
121+
122+
return {
123+
authorizedKeysTarget: path.join(stagingDir, "authorized-keys", authorizedKeysBase),
124+
envGlobalTarget: path.join(stagingDir, "env-global", envGlobalBase),
125+
envProjectTarget: path.join(stagingDir, "env-project", envProjectBase),
126+
projectCodexTarget: path.join(stagingDir, "project-auth", "codex"),
127+
projectClaudeTarget: path.join(stagingDir, "project-auth", "claude"),
128+
sharedCodexTarget: path.join(stagingDir, "shared-auth", "codex")
129+
}
130+
}
131+
132+
const ensureBootstrapSnapshotLayout = (
133+
path: Path.Path,
134+
fs: FileSystem.FileSystem,
135+
targets: BootstrapSnapshotTargets
136+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
137+
Effect.gen(function*(_) {
138+
yield* _(fs.makeDirectory(path.dirname(targets.authorizedKeysTarget), { recursive: true }))
139+
yield* _(fs.makeDirectory(path.dirname(targets.envGlobalTarget), { recursive: true }))
140+
yield* _(fs.makeDirectory(path.dirname(targets.envProjectTarget), { recursive: true }))
141+
yield* _(fs.makeDirectory(targets.projectCodexTarget, { recursive: true }))
142+
yield* _(fs.makeDirectory(targets.projectClaudeTarget, { recursive: true }))
143+
yield* _(fs.makeDirectory(targets.sharedCodexTarget, { recursive: true }))
144+
})
145+
146+
const copyBootstrapSnapshotFiles = (
147+
fs: FileSystem.FileSystem,
148+
path: Path.Path,
149+
sources: BootstrapSnapshotSources,
150+
targets: BootstrapSnapshotTargets
151+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
152+
Effect.gen(function*(_) {
153+
yield* _(copyFileIfPresent(fs, path, sources.authorizedKeysSource, targets.authorizedKeysTarget))
154+
yield* _(copyFileIfPresent(fs, path, sources.envGlobalSource, targets.envGlobalTarget))
155+
yield* _(copyFileIfPresent(fs, path, sources.envProjectSource, targets.envProjectTarget))
156+
})
157+
158+
const copyBootstrapSnapshotAuthDirs = (
159+
fs: FileSystem.FileSystem,
160+
path: Path.Path,
161+
sources: BootstrapSnapshotSources,
162+
targets: BootstrapSnapshotTargets
163+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
164+
Effect.gen(function*(_) {
165+
yield* _(copyDirRecursive(fs, path, sources.codexAuthSource, targets.projectCodexTarget))
166+
yield* _(copyDirRecursive(fs, path, sources.claudeAuthSource, targets.projectClaudeTarget))
167+
yield* _(copyDirRecursive(fs, path, sources.codexSharedAuthSource, targets.sharedCodexTarget))
168+
})
169+
77170
const stageBootstrapSnapshot = (
78171
stagingDir: string,
79172
projectDir: string,
80-
config: Pick<
81-
TemplateConfig,
82-
"authorizedKeysPath" | "envGlobalPath" | "envProjectPath" | "codexAuthPath" | "codexSharedAuthPath"
83-
>
173+
config: BootstrapSeedConfig
84174
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
85175
Effect.gen(function*(_) {
86176
const fs = yield* _(FileSystem.FileSystem)
87177
const path = yield* _(Path.Path)
88178

89-
const authorizedKeysSource = resolvePathFromBase(path, projectDir, config.authorizedKeysPath)
90-
const envGlobalSource = resolvePathFromBase(path, projectDir, config.envGlobalPath)
91-
const envProjectSource = resolvePathFromBase(path, projectDir, config.envProjectPath)
92-
const codexAuthSource = resolvePathFromBase(path, projectDir, config.codexAuthPath)
93-
const codexSharedAuthSource = resolvePathFromBase(path, projectDir, config.codexSharedAuthPath)
94-
const claudeAuthSource = path.join(path.dirname(codexAuthSource), "claude")
95-
96-
const authorizedKeysBase = config.authorizedKeysPath.replaceAll("\\", "/").split("/").at(-1) ?? "authorized_keys"
97-
const envGlobalBase = config.envGlobalPath.replaceAll("\\", "/").split("/").at(-1) ?? "global.env"
98-
const envProjectBase = config.envProjectPath.replaceAll("\\", "/").split("/").at(-1) ?? "project.env"
99-
100-
yield* _(fs.makeDirectory(path.join(stagingDir, "authorized-keys"), { recursive: true }))
101-
yield* _(fs.makeDirectory(path.join(stagingDir, "env-global"), { recursive: true }))
102-
yield* _(fs.makeDirectory(path.join(stagingDir, "env-project"), { recursive: true }))
103-
yield* _(fs.makeDirectory(path.join(stagingDir, "project-auth", "codex"), { recursive: true }))
104-
yield* _(fs.makeDirectory(path.join(stagingDir, "project-auth", "claude"), { recursive: true }))
105-
yield* _(fs.makeDirectory(path.join(stagingDir, "shared-auth", "codex"), { recursive: true }))
106-
107-
yield* _(
108-
copyFileIfPresent(
109-
fs,
110-
path,
111-
authorizedKeysSource,
112-
path.join(stagingDir, "authorized-keys", authorizedKeysBase)
113-
)
114-
)
115-
yield* _(
116-
copyFileIfPresent(
117-
fs,
118-
path,
119-
envGlobalSource,
120-
path.join(stagingDir, "env-global", envGlobalBase)
121-
)
122-
)
123-
yield* _(
124-
copyFileIfPresent(
125-
fs,
126-
path,
127-
envProjectSource,
128-
path.join(stagingDir, "env-project", envProjectBase)
129-
)
130-
)
131-
yield* _(copyDirRecursive(fs, path, codexAuthSource, path.join(stagingDir, "project-auth", "codex")))
132-
yield* _(copyDirRecursive(fs, path, claudeAuthSource, path.join(stagingDir, "project-auth", "claude")))
133-
yield* _(copyDirRecursive(fs, path, codexSharedAuthSource, path.join(stagingDir, "shared-auth", "codex")))
179+
const sources = resolveBootstrapSnapshotSources(path, projectDir, config)
180+
const targets = resolveBootstrapSnapshotTargets(path, stagingDir, config)
181+
182+
yield* _(ensureBootstrapSnapshotLayout(path, fs, targets))
183+
yield* _(copyBootstrapSnapshotFiles(fs, path, sources, targets))
184+
yield* _(copyBootstrapSnapshotAuthDirs(fs, path, sources, targets))
134185
})
135186

136187
export const ensureProjectBootstrapVolumeReady = (

packages/lib/tests/usecases/create-project-open-ssh.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ const isDockerComposeUp = (cmd: RecordedCommand): boolean =>
106106

107107
const isBootstrapSeed = (cmd: RecordedCommand): boolean =>
108108
cmd.command === "bash" &&
109-
cmd.args[0] === "-lc" &&
109+
(cmd.args[0] === "-c" || cmd.args[0] === "-lc") &&
110110
(cmd.args[1] ?? "").includes("docker run --rm -i -v 'dg-test-home-bootstrap:/target' alpine:3.20")
111111

112112
const decideExitCode = (cmd: RecordedCommand): number => {

packages/lib/tests/usecases/projects-up.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const isDockerVolumeCreate = (cmd: RecordedCommand): boolean =>
6464

6565
const isBootstrapSeed = (cmd: RecordedCommand): boolean =>
6666
cmd.command === "bash" &&
67-
cmd.args[0] === "-lc" &&
67+
(cmd.args[0] === "-c" || cmd.args[0] === "-lc") &&
6868
(cmd.args[1] ?? "").includes("docker run --rm -i -v 'dg-test-home-bootstrap:/target' alpine:3.20")
6969

7070
const isDockerInspectBridgeIp = (cmd: RecordedCommand): boolean =>

scripts/e2e/_lib.sh

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,35 @@ EOF
3232
dg_write_docker_host_file() {
3333
local host_path="$1"
3434
local mode="${2:-}"
35+
local host_uid
36+
local host_gid
3537

3638
local host_dir
3739
local host_name
3840
host_dir="$(dirname "$host_path")"
3941
host_name="$(basename "$host_path")"
42+
host_uid="$(id -u)"
43+
host_gid="$(id -g)"
4044

4145
if [[ -n "$mode" ]] && [[ ! "$mode" =~ ^[0-7]{3,4}$ ]]; then
4246
echo "e2e: invalid file mode: $mode" >&2
4347
return 1
4448
fi
4549

4650
if [[ -n "$mode" ]]; then
47-
docker run --rm -i -v "$host_dir":/mnt ubuntu:24.04 \
48-
bash -lc "cat > \"/mnt/$host_name\" && chmod \"$mode\" \"/mnt/$host_name\""
51+
docker run --rm -i \
52+
-e HOST_UID="$host_uid" \
53+
-e HOST_GID="$host_gid" \
54+
-v "$host_dir":/mnt ubuntu:24.04 \
55+
bash -lc "cat > \"/mnt/$host_name\" && chmod \"$mode\" \"/mnt/$host_name\" && chown \"\$HOST_UID:\$HOST_GID\" \"/mnt/$host_name\""
4956
return 0
5057
fi
5158

52-
docker run --rm -i -v "$host_dir":/mnt ubuntu:24.04 \
53-
bash -lc "cat > \"/mnt/$host_name\""
59+
docker run --rm -i \
60+
-e HOST_UID="$host_uid" \
61+
-e HOST_GID="$host_gid" \
62+
-v "$host_dir":/mnt ubuntu:24.04 \
63+
bash -lc "cat > \"/mnt/$host_name\" && chown \"\$HOST_UID:\$HOST_GID\" \"/mnt/$host_name\""
5464
}
5565

5666
# Ensure the calling script can run `docker` (and therefore docker-git) in a

0 commit comments

Comments
 (0)