From fb68a4ed76a984455a1f241b956fb01e7480fdad Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 16 Jun 2026 10:48:20 +0000 Subject: [PATCH 1/4] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/ProverCoderAI/docker-git/issues/408 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 00000000..64cc6054 --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-06-16T10:48:20.295Z for PR creation at branch issue-408-5d575dc8d85e for issue https://github.com/ProverCoderAI/docker-git/issues/408 \ No newline at end of file From 2261c1cb7e323cfba2850ec508b3b88472fe2bdf Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 16 Jun 2026 11:14:03 +0000 Subject: [PATCH 2/4] fix(entrypoint): clone into the dev-owned app folder The standalone base image's Dockerfile prepares and chowns /home/dev/app to the unprivileged 'dev' user, but entrypoint.sh defaulted TARGET_DIR to /work/app. Since the auto-clone runs as 'su - dev', cloning into the root-created /work/app failed with permission denied, so the repo never landed in the prepared 'app' folder. - Default TARGET_DIR to /home/dev/app to match the Dockerfile's app folder. - chown the resolved TARGET_DIR to dev so overrides outside /home/dev also work. - Add a regression test pinning the entrypoint default to the chowned app dir. Fixes #408 --- .gitkeep | 1 - entrypoint.sh | 7 ++- .../shell/entrypoint-clone-target.test.ts | 60 +++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) delete mode 100644 .gitkeep create mode 100644 packages/lib/tests/shell/entrypoint-clone-target.test.ts diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 64cc6054..00000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-06-16T10:48:20.295Z for PR creation at branch issue-408-5d575dc8d85e for issue https://github.com/ProverCoderAI/docker-git/issues/408 \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 8024b912..d10c7265 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -49,7 +49,7 @@ docker_git_repair_dns || true REPO_URL="${REPO_URL:-}" REPO_REF="${REPO_REF:-}" -TARGET_DIR="${TARGET_DIR:-/work/app}" +TARGET_DIR="${TARGET_DIR:-/home/dev/app}" # 1) Authorized keys are mounted from host at /authorized_keys mkdir -p /home/dev/.ssh @@ -70,6 +70,11 @@ chown -R dev:dev /home/dev/.codex if [[ -n "$REPO_URL" && ! -d "$TARGET_DIR/.git" ]]; then mkdir -p "$TARGET_DIR" chown -R dev:dev /home/dev + # git clone runs as `su - dev`, so the target must be writable by dev even when + # TARGET_DIR is overridden to a path outside /home/dev (otherwise clone fails silently). + if [[ "$TARGET_DIR" != "/" ]]; then + chown -R dev:dev "$TARGET_DIR" + fi if [[ -n "$REPO_REF" ]]; then su - dev -c "git clone --branch '$REPO_REF' '$REPO_URL' '$TARGET_DIR'" diff --git a/packages/lib/tests/shell/entrypoint-clone-target.test.ts b/packages/lib/tests/shell/entrypoint-clone-target.test.ts new file mode 100644 index 00000000..da4835e7 --- /dev/null +++ b/packages/lib/tests/shell/entrypoint-clone-target.test.ts @@ -0,0 +1,60 @@ +// CHANGE: pin the standalone base-image clone target to the dev-owned app folder +// WHY: entrypoint runs `git clone` as `su - dev`; cloning into a root-owned dir (e.g. /work/app) +// fails with permission denied, so the repo never lands in the prepared `app` folder +// QUOTE(ТЗ): "Почему-то при docker-git clone не делается git clone в папку app" +// REF: issue-408 +// SOURCE: n/a +// FORMAT THEOREM: defaultTargetDir(entrypoint) = appDir(Dockerfile) ∧ appDir ⊂ chown(dev, /home/dev) +// PURITY: SHELL (reads repository source files) +// INVARIANT: clone target is owned by the unprivileged clone user +// COMPLEXITY: O(|file|) +import { describe, expect, it } from "@effect/vitest" +import { readFileSync } from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +// packages/lib/tests/shell -> repository root +const repoRoot = path.resolve(__dirname, "..", "..", "..", "..") + +const readRepoFile = (relativePath: string): string => + readFileSync(path.join(repoRoot, relativePath), "utf8") + +describe("standalone base-image clone target", () => { + const entrypoint = readRepoFile("entrypoint.sh") + const dockerfile = readRepoFile("Dockerfile") + + // CHANGE: derive the dev home that the Dockerfile recursively chowns to the clone user + // WHY: `git clone` runs as `su - dev`, so the target must live under a dev-owned path + // PURITY: CORE + // INVARIANT: chownedHome captures the `chown -R dev:dev ` argument + const chownedHome = (() => { + const match = dockerfile.match(/chown -R dev:dev (\/home\/dev)\b/) + return match?.[1] + })() + + // CHANGE: derive the default TARGET_DIR the entrypoint clones into + // WHY: the bug is a wrong default that points outside the dev-owned home + // PURITY: CORE + // INVARIANT: defaultTargetDir captures `TARGET_DIR="${TARGET_DIR:-}"` + const defaultTargetDir = (() => { + const match = entrypoint.match(/TARGET_DIR="\$\{TARGET_DIR:-([^}]+)\}"/) + return match?.[1] + })() + + it("prepares an app folder under the dev home in the Dockerfile", () => { + expect(dockerfile).toContain("mkdir -p /home/dev/app") + expect(chownedHome).toBe("/home/dev") + }) + + it("defaults the clone target to the prepared app folder", () => { + expect(defaultTargetDir).toBe("/home/dev/app") + }) + + it("keeps the clone target under the dev-owned home so `su - dev` can write into it", () => { + expect(defaultTargetDir).toBeDefined() + expect(chownedHome).toBeDefined() + expect(`${defaultTargetDir}/`.startsWith(`${chownedHome}/`)).toBe(true) + }) +}) From b38ae32e3d473708057a10dd998295831eb73ee7 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 16 Jun 2026 11:14:36 +0000 Subject: [PATCH 3/4] chore(changeset): add patch for entrypoint clone target fix --- .changeset/entrypoint-clone-app-folder.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/entrypoint-clone-app-folder.md diff --git a/.changeset/entrypoint-clone-app-folder.md b/.changeset/entrypoint-clone-app-folder.md new file mode 100644 index 00000000..38db340e --- /dev/null +++ b/.changeset/entrypoint-clone-app-folder.md @@ -0,0 +1,12 @@ +--- +"@prover-coder-ai/docker-git": patch +--- + +Fix the standalone base image cloning the repo outside the prepared `app` folder. + +The Dockerfile prepares and chowns `/home/dev/app` to the unprivileged `dev` +user, but `entrypoint.sh` defaulted `TARGET_DIR` to `/work/app`. Because the +auto-clone runs as `su - dev`, cloning into the root-created `/work/app` failed +with permission denied, so the repository never landed in the `app` folder. +The default now points at `/home/dev/app`, and the resolved `TARGET_DIR` is +chowned to `dev` so overrides outside `/home/dev` keep working too. From 9239b9e74081197e55df66fcfe973d6f7f08580b Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 16 Jun 2026 11:26:00 +0000 Subject: [PATCH 4/4] test(entrypoint): use @effect/platform FileSystem to satisfy Effect-TS lint --- .../shell/entrypoint-clone-target.test.ts | 95 ++++++++++--------- 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/packages/lib/tests/shell/entrypoint-clone-target.test.ts b/packages/lib/tests/shell/entrypoint-clone-target.test.ts index da4835e7..2aae1e7a 100644 --- a/packages/lib/tests/shell/entrypoint-clone-target.test.ts +++ b/packages/lib/tests/shell/entrypoint-clone-target.test.ts @@ -5,56 +5,65 @@ // REF: issue-408 // SOURCE: n/a // FORMAT THEOREM: defaultTargetDir(entrypoint) = appDir(Dockerfile) ∧ appDir ⊂ chown(dev, /home/dev) -// PURITY: SHELL (reads repository source files) +// PURITY: SHELL (reads repository source files via @effect/platform FileSystem) // INVARIANT: clone target is owned by the unprivileged clone user // COMPLEXITY: O(|file|) +import * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" +import { NodeContext } from "@effect/platform-node" import { describe, expect, it } from "@effect/vitest" -import { readFileSync } from "node:fs" -import path from "node:path" -import { fileURLToPath } from "node:url" +import { Effect } from "effect" -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -// packages/lib/tests/shell -> repository root -const repoRoot = path.resolve(__dirname, "..", "..", "..", "..") +// CHANGE: derive the dev home that the Dockerfile recursively chowns to the clone user +// WHY: `git clone` runs as `su - dev`, so the target must live under a dev-owned path +// PURITY: CORE +// INVARIANT: returns the `chown -R dev:dev ` argument, or "" when absent +const matchChownedHome = (dockerfile: string): string => + dockerfile.match(/chown -R dev:dev (\/home\/dev)\b/)?.[1] ?? "" -const readRepoFile = (relativePath: string): string => - readFileSync(path.join(repoRoot, relativePath), "utf8") +// CHANGE: derive the default TARGET_DIR the entrypoint clones into +// WHY: the bug is a wrong default that points outside the dev-owned home +// PURITY: CORE +// INVARIANT: returns the `TARGET_DIR="${TARGET_DIR:-}"` value, or "" when absent +const matchDefaultTargetDir = (entrypoint: string): string => + entrypoint.match(/TARGET_DIR="\$\{TARGET_DIR:-([^}]+)\}"/)?.[1] ?? "" -describe("standalone base-image clone target", () => { - const entrypoint = readRepoFile("entrypoint.sh") - const dockerfile = readRepoFile("Dockerfile") - - // CHANGE: derive the dev home that the Dockerfile recursively chowns to the clone user - // WHY: `git clone` runs as `su - dev`, so the target must live under a dev-owned path - // PURITY: CORE - // INVARIANT: chownedHome captures the `chown -R dev:dev ` argument - const chownedHome = (() => { - const match = dockerfile.match(/chown -R dev:dev (\/home\/dev)\b/) - return match?.[1] - })() - - // CHANGE: derive the default TARGET_DIR the entrypoint clones into - // WHY: the bug is a wrong default that points outside the dev-owned home - // PURITY: CORE - // INVARIANT: defaultTargetDir captures `TARGET_DIR="${TARGET_DIR:-}"` - const defaultTargetDir = (() => { - const match = entrypoint.match(/TARGET_DIR="\$\{TARGET_DIR:-([^}]+)\}"/) - return match?.[1] - })() +// CHANGE: read the repo-root entrypoint + Dockerfile through the Effect FileSystem service +// WHY: lint forbids node:* imports; @effect/platform is the sanctioned IO boundary +// PURITY: SHELL +// EFFECT: Effect<{ entrypoint, dockerfile }, PlatformError | BadArgument, FileSystem | Path> +const readRepoSources = Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const path = yield* _(Path.Path) + const here = yield* _(path.fromFileUrl(new URL(import.meta.url))) + // packages/lib/tests/shell -> repository root + const repoRoot = path.resolve(path.dirname(here), "..", "..", "..", "..") + const entrypoint = yield* _(fs.readFileString(path.join(repoRoot, "entrypoint.sh"))) + const dockerfile = yield* _(fs.readFileString(path.join(repoRoot, "Dockerfile"))) + return { dockerfile, entrypoint } +}) - it("prepares an app folder under the dev home in the Dockerfile", () => { - expect(dockerfile).toContain("mkdir -p /home/dev/app") - expect(chownedHome).toBe("/home/dev") - }) +describe("standalone base-image clone target", () => { + it.effect("prepares an app folder under the dev home in the Dockerfile", () => + Effect.gen(function*(_) { + const { dockerfile } = yield* _(readRepoSources) + expect(dockerfile).toContain("mkdir -p /home/dev/app") + expect(matchChownedHome(dockerfile)).toBe("/home/dev") + }).pipe(Effect.provide(NodeContext.layer))) - it("defaults the clone target to the prepared app folder", () => { - expect(defaultTargetDir).toBe("/home/dev/app") - }) + it.effect("defaults the clone target to the prepared app folder", () => + Effect.gen(function*(_) { + const { entrypoint } = yield* _(readRepoSources) + expect(matchDefaultTargetDir(entrypoint)).toBe("/home/dev/app") + }).pipe(Effect.provide(NodeContext.layer))) - it("keeps the clone target under the dev-owned home so `su - dev` can write into it", () => { - expect(defaultTargetDir).toBeDefined() - expect(chownedHome).toBeDefined() - expect(`${defaultTargetDir}/`.startsWith(`${chownedHome}/`)).toBe(true) - }) + it.effect("keeps the clone target under the dev-owned home so `su - dev` can write into it", () => + Effect.gen(function*(_) { + const { dockerfile, entrypoint } = yield* _(readRepoSources) + const chownedHome = matchChownedHome(dockerfile) + const defaultTargetDir = matchDefaultTargetDir(entrypoint) + expect(chownedHome).not.toBe("") + expect(defaultTargetDir).not.toBe("") + expect(`${defaultTargetDir}/`.startsWith(`${chownedHome}/`)).toBe(true) + }).pipe(Effect.provide(NodeContext.layer))) })