diff --git a/.changeset/entrypoint-tilde-clone-target.md b/.changeset/entrypoint-tilde-clone-target.md new file mode 100644 index 00000000..017bb74c --- /dev/null +++ b/.changeset/entrypoint-tilde-clone-target.md @@ -0,0 +1,15 @@ +--- +"@prover-coder-ai/docker-git": patch +--- + +Fix `docker-git clone` leaving the workspace `app` folder empty when `TARGET_DIR` +is a tilde path. + +The generated entrypoint runs as `root` (sshd), so `$HOME` resolves to `/root`. +When a `~`/`~/...` `TARGET_DIR` reached the entrypoint (e.g. via the `TARGET_DIR` +env override), it was expanded against `$HOME`, resolving to `/root/app`. Because +the auto-clone runs as `su - `, cloning into the root-owned `/root/app` +failed with "permission denied", so the repository never landed in the prepared +home and the workspace `app` folder stayed empty. The tilde is now expanded +against the unprivileged user's home `/home/`, so the clone always lands +in the dev-owned workspace. diff --git a/packages/container/src/core/templates-entrypoint/base.ts b/packages/container/src/core/templates-entrypoint/base.ts index e29212eb..0ad47619 100644 --- a/packages/container/src/core/templates-entrypoint/base.ts +++ b/packages/container/src/core/templates-entrypoint/base.ts @@ -17,9 +17,9 @@ REPO_REF="\${REPO_REF:-}" FORK_REPO_URL="\${FORK_REPO_URL:-}" ${renderTargetDirDefault(config)} if [[ "$TARGET_DIR" == "~" ]]; then - TARGET_DIR="$HOME" + TARGET_DIR="/home/${config.sshUser}" elif [[ "$TARGET_DIR" == "~/"* ]]; then - TARGET_DIR="$HOME\${TARGET_DIR:1}" + TARGET_DIR="/home/${config.sshUser}\${TARGET_DIR:1}" fi CLAUDE_AUTH_LABEL="\${CLAUDE_AUTH_LABEL:-}" CODEX_AUTH_LABEL="\${CODEX_AUTH_LABEL:-}" diff --git a/packages/container/tests/core/templates.test.ts b/packages/container/tests/core/templates.test.ts index d31ba385..4fb97ef2 100644 --- a/packages/container/tests/core/templates.test.ts +++ b/packages/container/tests/core/templates.test.ts @@ -465,6 +465,38 @@ describe("renderEntrypoint clone cache", () => { }) }) +describe("renderEntrypoint tilde target dir expansion", () => { + // CHANGE: assert runtime `~`/`~/...` TARGET_DIR overrides resolve to the dev-owned home + // WHY: the entrypoint runs as root (sshd), so `$HOME` is /root; expanding a tilde TARGET_DIR + // against `$HOME` resolved the clone target to /root/app, which `su - dev` cannot write, + // so `git clone` failed and the workspace `app` folder stayed EMPTY (issue #413) + // QUOTE(ТЗ): "Почему-то при docker-git clone не делается git clone в папку app" + // REF: issue-413 + // FORMAT THEOREM: expand("~") = /home/ ∧ expand("~/p") = /home//p + it("expands a bare `~` against the dev home, not root's $HOME", () => { + const entrypoint = renderEntrypoint(makeTemplateConfig({ sshUser: "dev" })) + + expect(entrypoint).toContain('if [[ "$TARGET_DIR" == "~" ]]; then') + expect(entrypoint).toContain('TARGET_DIR="/home/dev"') + expect(entrypoint).toContain('TARGET_DIR="/home/dev${TARGET_DIR:1}"') + expect(entrypoint).not.toContain('TARGET_DIR="$HOME"') + expect(entrypoint).not.toContain('TARGET_DIR="$HOME${TARGET_DIR:1}"') + }) + + it("expands the tilde against the configured ssh user for generated configs", () => { + fc.assert( + fc.property(generatedTemplateConfigArbitrary, (config) => { + const entrypoint = renderEntrypoint(config) + + expect(entrypoint).toContain(`TARGET_DIR="/home/${config.sshUser}"`) + expect(entrypoint).toContain(`TARGET_DIR="/home/${config.sshUser}\${TARGET_DIR:1}"`) + expect(entrypoint).not.toContain('TARGET_DIR="$HOME"') + expect(entrypoint).not.toContain('TARGET_DIR="$HOME${TARGET_DIR:1}"') + }) + ) + }) +}) + describe("renderEntrypointGitHooks", () => { it("installs pre-push protection checks, plan sync, and a global git post-push runtime", () => { const hooks = renderEntrypointGitHooks()