From 6117692279f665f55a8ce4822b944594465484ec Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 18 May 2026 15:24:35 +0000 Subject: [PATCH 01/13] feat(coder/modules/git-clone): add support for recursive submodule cloning and parallel jobs --- registry/coder/modules/git-clone/README.md | 44 +++++-- registry/coder/modules/git-clone/main.test.ts | 107 +++++++++++++++--- registry/coder/modules/git-clone/main.tf | 26 ++++- registry/coder/modules/git-clone/run.sh | 26 +++-- 4 files changed, 166 insertions(+), 37 deletions(-) diff --git a/registry/coder/modules/git-clone/README.md b/registry/coder/modules/git-clone/README.md index 3336770f5..4490521fd 100644 --- a/registry/coder/modules/git-clone/README.md +++ b/registry/coder/modules/git-clone/README.md @@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" } @@ -28,7 +28,7 @@ module "git-clone" { module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" base_dir = "~/projects/coder" @@ -43,7 +43,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" } @@ -70,7 +70,7 @@ data "coder_parameter" "git_repo" { module "git_clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = data.coder_parameter.git_repo.value } @@ -105,7 +105,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = "https://github.example.com/coder/coder/tree/feat/example" git_providers = { @@ -125,7 +125,7 @@ To GitLab clone with a specific branch like `feat/example` module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = "https://gitlab.com/coder/coder/-/tree/feat/example" } @@ -137,7 +137,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com` module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = "https://gitlab.example.com/coder/coder/-/tree/feat/example" git_providers = { @@ -159,7 +159,7 @@ For example, to clone the `feat/example` branch: module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" branch_name = "feat/example" @@ -177,7 +177,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder: module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" folder_name = "coder-dev" @@ -196,13 +196,33 @@ If not defined, the default, `0`, performs a full clone. module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" depth = 1 } ``` +## Recurse submodules + +Set `recurse_submodules = true` to initialize and clone submodules during the +clone (equivalent to `git clone --recurse-submodules`). + +Pair it with `clone_jobs` to fetch submodules in parallel (equivalent to +`git clone --jobs `) and speed up workspace start. + +```tf +module "git-clone" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/git-clone/coder" + version = "1.4.0" + agent_id = coder_agent.example.id + url = "https://github.com/coder/coder" + recurse_submodules = true + clone_jobs = 8 +} +``` + ## Pre-clone script Run a custom script before cloning the repository by setting the `pre_clone_script` variable. @@ -212,7 +232,7 @@ This is useful for preparing the environment or validating prerequisites before module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" pre_clone_script = <<-EOT @@ -235,7 +255,7 @@ This is useful for running initialization tasks like installing dependencies or module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" post_clone_script = <<-EOT diff --git a/registry/coder/modules/git-clone/main.test.ts b/registry/coder/modules/git-clone/main.test.ts index af900eeff..4a30aa4f3 100644 --- a/registry/coder/modules/git-clone/main.test.ts +++ b/registry/coder/modules/git-clone/main.test.ts @@ -1,11 +1,37 @@ import { describe, expect, it } from "bun:test"; import { - executeScriptInContainer, + execContainer, + findResourceInstance, + runContainer, runTerraformApply, runTerraformInit, testRequiredVariables, + type scriptOutput, + type TerraformState, } from "~test"; +// The clone script uses bash arrays, which busybox `sh` (alpine's default +// shell) cannot parse. Install bash in the container, then run the script +// with bash. The optional `before` setup step still runs with `sh`. +const executeScriptInContainer = async ( + state: TerraformState, + image: string, + before?: string, +): Promise => { + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer(image); + await execContainer(id, ["sh", "-c", "apk add --no-cache bash >/dev/null"]); + if (before) { + await execContainer(id, ["sh", "-c", before]); + } + const resp = await execContainer(id, ["bash", "-c", instance.script]); + return { + exitCode: resp.exitCode, + stdout: resp.stdout.trim().split("\n"), + stderr: resp.stderr.trim().split("\n"), + }; +}; + describe("git-clone", async () => { await runTerraformInit(import.meta.dir); @@ -31,8 +57,8 @@ describe("git-clone", async () => { }); const output = await executeScriptInContainer(state, "alpine/git"); expect(output.stdout).toEqual([ - "Creating directory ~/fake-url...", - "Cloning fake-url to ~/fake-url...", + "Creating directory /root/fake-url...", + "Cloning fake-url to /root/fake-url...", ]); expect(output.stderr.join(" ")).toContain("fatal"); expect(output.stderr.join(" ")).toContain("fake-url"); @@ -207,8 +233,8 @@ describe("git-clone", async () => { const output = await executeScriptInContainer(state, "alpine/git"); expect(output.exitCode).toBe(0); expect(output.stdout).toEqual([ - "Creating directory ~/repo-tests.log...", - "Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...", + "Creating directory /root/repo-tests.log...", + "Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...", ]); }); @@ -220,8 +246,8 @@ describe("git-clone", async () => { const output = await executeScriptInContainer(state, "alpine/git"); expect(output.exitCode).toBe(0); expect(output.stdout).toEqual([ - "Creating directory ~/repo-tests.log...", - "Cloning https://gitlab.com/mike.brew/repo-tests.log to ~/repo-tests.log on branch feat/branch...", + "Creating directory /root/repo-tests.log...", + "Cloning https://gitlab.com/mike.brew/repo-tests.log to /root/repo-tests.log on branch feat/branch...", ]); }); @@ -241,8 +267,8 @@ describe("git-clone", async () => { const output = await executeScriptInContainer(state, "alpine/git"); expect(output.exitCode).toBe(0); expect(output.stdout).toEqual([ - "Creating directory ~/repo-tests.log...", - "Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...", + "Creating directory /root/repo-tests.log...", + "Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...", ]); }); @@ -256,7 +282,6 @@ describe("git-clone", async () => { const output = await executeScriptInContainer( state, "alpine/git", - "sh", "mkdir -p /tmp/fake-url && echo 'existing' > /tmp/fake-url/file.txt", ); expect(output.stdout).toContain("Running post-clone script..."); @@ -272,7 +297,7 @@ describe("git-clone", async () => { const output = await executeScriptInContainer(state, "alpine/git"); expect(output.stdout).toContain("Running pre-clone script..."); expect(output.stdout).toContain("Pre-clone script executed"); - expect(output.stdout).toContain("Cloning fake-url to ~/fake-url..."); + expect(output.stdout).toContain("Cloning fake-url to /root/fake-url..."); }); it("fails when pre-clone script fails", async () => { @@ -285,7 +310,64 @@ describe("git-clone", async () => { expect(output.exitCode).toBe(42); expect(output.stdout).toContain("Running pre-clone script..."); expect(output.stdout).toContain("Pre-clone script failed"); - expect(output.stdout).not.toContain("Cloning fake-url to ~/fake-url..."); + expect(output.stdout).not.toContain( + "Cloning fake-url to /root/fake-url...", + ); + }); + + it("defaults recurse_submodules to false and clone_jobs to 0", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "fake-url", + }); + const script = findResourceInstance(state, "coder_script").script; + expect(script).toContain('RECURSE_SUBMODULES="false"'); + expect(script).toContain('CLONE_JOBS="0"'); + }); + + it("sets RECURSE_SUBMODULES=true when recurse_submodules is enabled", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "fake-url", + recurse_submodules: "true", + }); + const script = findResourceInstance(state, "coder_script").script; + expect(script).toContain('RECURSE_SUBMODULES="true"'); + }); + + it("sets CLONE_JOBS when clone_jobs > 0", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "fake-url", + recurse_submodules: "true", + clone_jobs: "8", + }); + const script = findResourceInstance(state, "coder_script").script; + expect(script).toContain('CLONE_JOBS="8"'); + }); + + it("rejects non-positive clone_jobs", async () => { + const t = async () => { + await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "fake-url", + clone_jobs: "-1", + }); + }; + expect(t).toThrow("clone_jobs must be a positive integer when set."); + }); + + it("rejects clone_jobs without recurse_submodules", async () => { + const t = async () => { + await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "fake-url", + clone_jobs: "4", + }); + }; + expect(t).toThrow( + "clone_jobs only affects submodule fetching, so it requires recurse_submodules", + ); }); it("fails when post-clone script fails", async () => { @@ -298,7 +380,6 @@ describe("git-clone", async () => { const output = await executeScriptInContainer( state, "alpine/git", - "sh", "mkdir -p /tmp/fake-url && echo 'existing' > /tmp/fake-url/file.txt", ); expect(output.exitCode).toBe(43); diff --git a/registry/coder/modules/git-clone/main.tf b/registry/coder/modules/git-clone/main.tf index 1fb28a4d9..ec5f13395 100644 --- a/registry/coder/modules/git-clone/main.tf +++ b/registry/coder/modules/git-clone/main.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.9" required_providers { coder = { @@ -62,6 +62,26 @@ variable "depth" { default = 0 } +variable "recurse_submodules" { + description = "If true, clone submodules recursively (equivalent to `git clone --recurse-submodules`)." + type = bool + default = false +} + +variable "clone_jobs" { + description = "If set, fetch submodules in parallel using this many jobs (equivalent to `git clone --jobs `). Only takes effect when `recurse_submodules = true`." + type = number + default = null + validation { + condition = var.clone_jobs == null || var.clone_jobs > 0 + error_message = "clone_jobs must be a positive integer when set." + } + validation { + condition = var.clone_jobs == null || var.recurse_submodules + error_message = "clone_jobs only affects submodule fetching, so it requires recurse_submodules = true." + } +} + variable "post_clone_script" { description = "Custom script to run after cloning the repository. Runs always after git clone, even if the repository already exists." type = string @@ -135,7 +155,9 @@ resource "coder_script" "git_clone" { CLONE_PATH = local.clone_path, REPO_URL : local.clone_url, BRANCH_NAME : local.branch_name, - DEPTH = var.depth, + DEPTH = var.depth, + RECURSE_SUBMODULES = tostring(var.recurse_submodules), + CLONE_JOBS = coalesce(var.clone_jobs, 0), POST_CLONE_SCRIPT : local.encoded_post_clone_script, PRE_CLONE_SCRIPT : local.encoded_pre_clone_script, }) diff --git a/registry/coder/modules/git-clone/run.sh b/registry/coder/modules/git-clone/run.sh index 76928a406..fb0d83b82 100644 --- a/registry/coder/modules/git-clone/run.sh +++ b/registry/coder/modules/git-clone/run.sh @@ -8,6 +8,8 @@ BRANCH_NAME="${BRANCH_NAME}" # Expand home if it's specified! CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}" DEPTH="${DEPTH}" +RECURSE_SUBMODULES="${RECURSE_SUBMODULES}" +CLONE_JOBS="${CLONE_JOBS}" POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}" PRE_CLONE_SCRIPT="${PRE_CLONE_SCRIPT}" @@ -46,23 +48,27 @@ if [ -n "$PRE_CLONE_SCRIPT" ]; then rm "$PRE_CLONE_TMP" fi +# Build optional git clone flags +CLONE_FLAGS=() +if [ "$DEPTH" -gt 0 ]; then + CLONE_FLAGS+=(--depth "$DEPTH") +fi +if [ "$RECURSE_SUBMODULES" = "true" ]; then + CLONE_FLAGS+=(--recurse-submodules) +fi +if [ "$CLONE_JOBS" -gt 0 ]; then + CLONE_FLAGS+=(--jobs "$CLONE_JOBS") +fi + # Check if the directory is empty # and if it is, clone the repo, otherwise skip cloning if [ -z "$(ls -A "$CLONE_PATH")" ]; then if [ -z "$BRANCH_NAME" ]; then echo "Cloning $REPO_URL to $CLONE_PATH..." - if [ "$DEPTH" -gt 0 ]; then - git clone --depth "$DEPTH" "$REPO_URL" "$CLONE_PATH" - else - git clone "$REPO_URL" "$CLONE_PATH" - fi + git clone "$${CLONE_FLAGS[@]}" "$REPO_URL" "$CLONE_PATH" else echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..." - if [ "$DEPTH" -gt 0 ]; then - git clone --depth "$DEPTH" -b "$BRANCH_NAME" "$REPO_URL" "$CLONE_PATH" - else - git clone "$REPO_URL" -b "$BRANCH_NAME" "$CLONE_PATH" - fi + git clone "$${CLONE_FLAGS[@]}" -b "$BRANCH_NAME" "$REPO_URL" "$CLONE_PATH" fi else echo "$CLONE_PATH already exists and isn't empty, skipping clone!" From 1fc855711b7a0a514c3c9a178b1cd8b0a67cec27 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 19 May 2026 03:17:55 +0000 Subject: [PATCH 02/13] feat(run): log git clone command with flags and branch information --- registry/coder/modules/git-clone/main.test.ts | 4 ++++ registry/coder/modules/git-clone/run.sh | 2 ++ 2 files changed, 6 insertions(+) diff --git a/registry/coder/modules/git-clone/main.test.ts b/registry/coder/modules/git-clone/main.test.ts index 4a30aa4f3..0327417af 100644 --- a/registry/coder/modules/git-clone/main.test.ts +++ b/registry/coder/modules/git-clone/main.test.ts @@ -59,6 +59,7 @@ describe("git-clone", async () => { expect(output.stdout).toEqual([ "Creating directory /root/fake-url...", "Cloning fake-url to /root/fake-url...", + "Running: git clone fake-url /root/fake-url", ]); expect(output.stderr.join(" ")).toContain("fatal"); expect(output.stderr.join(" ")).toContain("fake-url"); @@ -235,6 +236,7 @@ describe("git-clone", async () => { expect(output.stdout).toEqual([ "Creating directory /root/repo-tests.log...", "Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...", + "Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log", ]); }); @@ -248,6 +250,7 @@ describe("git-clone", async () => { expect(output.stdout).toEqual([ "Creating directory /root/repo-tests.log...", "Cloning https://gitlab.com/mike.brew/repo-tests.log to /root/repo-tests.log on branch feat/branch...", + "Running: git clone -b feat/branch https://gitlab.com/mike.brew/repo-tests.log /root/repo-tests.log", ]); }); @@ -269,6 +272,7 @@ describe("git-clone", async () => { expect(output.stdout).toEqual([ "Creating directory /root/repo-tests.log...", "Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...", + "Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log", ]); }); diff --git a/registry/coder/modules/git-clone/run.sh b/registry/coder/modules/git-clone/run.sh index fb0d83b82..db0fb373e 100644 --- a/registry/coder/modules/git-clone/run.sh +++ b/registry/coder/modules/git-clone/run.sh @@ -65,9 +65,11 @@ fi if [ -z "$(ls -A "$CLONE_PATH")" ]; then if [ -z "$BRANCH_NAME" ]; then echo "Cloning $REPO_URL to $CLONE_PATH..." + echo "Running: git clone $${CLONE_FLAGS[*]} $REPO_URL $CLONE_PATH" git clone "$${CLONE_FLAGS[@]}" "$REPO_URL" "$CLONE_PATH" else echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..." + echo "Running: git clone $${CLONE_FLAGS[*]} -b $BRANCH_NAME $REPO_URL $CLONE_PATH" git clone "$${CLONE_FLAGS[@]}" -b "$BRANCH_NAME" "$REPO_URL" "$CLONE_PATH" fi else From d0629d2e28ec0c0965dd62b610f074afea0adcb5 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 19 May 2026 03:29:24 +0000 Subject: [PATCH 03/13] refactor(test): remove outdated comments regarding bash and busybox compatibility --- registry/coder/modules/git-clone/main.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/registry/coder/modules/git-clone/main.test.ts b/registry/coder/modules/git-clone/main.test.ts index 0327417af..9e1ddea12 100644 --- a/registry/coder/modules/git-clone/main.test.ts +++ b/registry/coder/modules/git-clone/main.test.ts @@ -10,9 +10,6 @@ import { type TerraformState, } from "~test"; -// The clone script uses bash arrays, which busybox `sh` (alpine's default -// shell) cannot parse. Install bash in the container, then run the script -// with bash. The optional `before` setup step still runs with `sh`. const executeScriptInContainer = async ( state: TerraformState, image: string, From 9d2e19de1a61032b8088abe930d2108240431d29 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 19 May 2026 06:32:24 +0000 Subject: [PATCH 04/13] feat(git-clone): replace depth, recurse_submodules, and clone_jobs with extra_args for flexible git clone options --- registry/coder/modules/git-clone/README.md | 37 +++++--------- registry/coder/modules/git-clone/main.test.ts | 48 +++---------------- registry/coder/modules/git-clone/main.tf | 36 ++++---------- registry/coder/modules/git-clone/run.sh | 16 ++----- 4 files changed, 31 insertions(+), 106 deletions(-) diff --git a/registry/coder/modules/git-clone/README.md b/registry/coder/modules/git-clone/README.md index 4490521fd..2d3a1a827 100644 --- a/registry/coder/modules/git-clone/README.md +++ b/registry/coder/modules/git-clone/README.md @@ -185,12 +185,12 @@ module "git-clone" { } ``` -## Git shallow clone +## Extra `git clone` arguments -Limit the clone history to speed-up workspace startup by setting `depth`. - -When `depth` is greater than `0` the module runs `git clone --depth `. -If not defined, the default, `0`, performs a full clone. +Pass any additional flags through `extra_args` (one element per argument). +This lets you enable anything `git clone` supports without the module having +to expose it explicitly — for example a shallow clone, submodules, parallel +fetches, or partial clones. ```tf module "git-clone" { @@ -199,27 +199,12 @@ module "git-clone" { version = "1.4.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" - depth = 1 -} -``` - -## Recurse submodules - -Set `recurse_submodules = true` to initialize and clone submodules during the -clone (equivalent to `git clone --recurse-submodules`). - -Pair it with `clone_jobs` to fetch submodules in parallel (equivalent to -`git clone --jobs `) and speed up workspace start. - -```tf -module "git-clone" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" - agent_id = coder_agent.example.id - url = "https://github.com/coder/coder" - recurse_submodules = true - clone_jobs = 8 + extra_args = [ + "--depth=1", + "--recurse-submodules", + "--jobs=8", + "--filter=blob:none", + ] } ``` diff --git a/registry/coder/modules/git-clone/main.test.ts b/registry/coder/modules/git-clone/main.test.ts index 9e1ddea12..93eaa32a5 100644 --- a/registry/coder/modules/git-clone/main.test.ts +++ b/registry/coder/modules/git-clone/main.test.ts @@ -316,58 +316,24 @@ describe("git-clone", async () => { ); }); - it("defaults recurse_submodules to false and clone_jobs to 0", async () => { + it("defaults extra_args to empty", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", url: "fake-url", }); const script = findResourceInstance(state, "coder_script").script; - expect(script).toContain('RECURSE_SUBMODULES="false"'); - expect(script).toContain('CLONE_JOBS="0"'); + expect(script).toContain('EXTRA_ARGS_B64=""'); }); - it("sets RECURSE_SUBMODULES=true when recurse_submodules is enabled", async () => { + it("passes extra_args to git clone", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", url: "fake-url", - recurse_submodules: "true", + extra_args: '["--recurse-submodules", "--jobs=8"]', }); - const script = findResourceInstance(state, "coder_script").script; - expect(script).toContain('RECURSE_SUBMODULES="true"'); - }); - - it("sets CLONE_JOBS when clone_jobs > 0", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - url: "fake-url", - recurse_submodules: "true", - clone_jobs: "8", - }); - const script = findResourceInstance(state, "coder_script").script; - expect(script).toContain('CLONE_JOBS="8"'); - }); - - it("rejects non-positive clone_jobs", async () => { - const t = async () => { - await runTerraformApply(import.meta.dir, { - agent_id: "foo", - url: "fake-url", - clone_jobs: "-1", - }); - }; - expect(t).toThrow("clone_jobs must be a positive integer when set."); - }); - - it("rejects clone_jobs without recurse_submodules", async () => { - const t = async () => { - await runTerraformApply(import.meta.dir, { - agent_id: "foo", - url: "fake-url", - clone_jobs: "4", - }); - }; - expect(t).toThrow( - "clone_jobs only affects submodule fetching, so it requires recurse_submodules", + const output = await executeScriptInContainer(state, "alpine/git"); + expect(output.stdout).toContain( + "Running: git clone --recurse-submodules --jobs=8 fake-url /root/fake-url", ); }); diff --git a/registry/coder/modules/git-clone/main.tf b/registry/coder/modules/git-clone/main.tf index ec5f13395..2a2f2e722 100644 --- a/registry/coder/modules/git-clone/main.tf +++ b/registry/coder/modules/git-clone/main.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.9" + required_version = ">= 1.0" required_providers { coder = { @@ -56,30 +56,10 @@ variable "folder_name" { default = "" } -variable "depth" { - description = "If > 0, perform a shallow clone using this depth." - type = number - default = 0 -} - -variable "recurse_submodules" { - description = "If true, clone submodules recursively (equivalent to `git clone --recurse-submodules`)." - type = bool - default = false -} - -variable "clone_jobs" { - description = "If set, fetch submodules in parallel using this many jobs (equivalent to `git clone --jobs `). Only takes effect when `recurse_submodules = true`." - type = number - default = null - validation { - condition = var.clone_jobs == null || var.clone_jobs > 0 - error_message = "clone_jobs must be a positive integer when set." - } - validation { - condition = var.clone_jobs == null || var.recurse_submodules - error_message = "clone_jobs only affects submodule fetching, so it requires recurse_submodules = true." - } +variable "extra_args" { + description = "Extra arguments to pass to `git clone`, one element per argument (e.g. `[\"--recurse-submodules\", \"--jobs=8\", \"--filter=blob:none\"]`)." + type = list(string) + default = [] } variable "post_clone_script" { @@ -117,6 +97,8 @@ locals { encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : "" # Encode the pre_clone_script for passing to the shell script encoded_pre_clone_script = var.pre_clone_script != null ? base64encode(var.pre_clone_script) : "" + # Encode extra clone args (newline-separated) so the shell script can split them into an array safely + encoded_extra_args = base64encode(join("\n", var.extra_args)) } output "repo_dir" { @@ -155,9 +137,7 @@ resource "coder_script" "git_clone" { CLONE_PATH = local.clone_path, REPO_URL : local.clone_url, BRANCH_NAME : local.branch_name, - DEPTH = var.depth, - RECURSE_SUBMODULES = tostring(var.recurse_submodules), - CLONE_JOBS = coalesce(var.clone_jobs, 0), + EXTRA_ARGS = local.encoded_extra_args, POST_CLONE_SCRIPT : local.encoded_post_clone_script, PRE_CLONE_SCRIPT : local.encoded_pre_clone_script, }) diff --git a/registry/coder/modules/git-clone/run.sh b/registry/coder/modules/git-clone/run.sh index db0fb373e..752e84bc2 100644 --- a/registry/coder/modules/git-clone/run.sh +++ b/registry/coder/modules/git-clone/run.sh @@ -7,9 +7,7 @@ CLONE_PATH="${CLONE_PATH}" BRANCH_NAME="${BRANCH_NAME}" # Expand home if it's specified! CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}" -DEPTH="${DEPTH}" -RECURSE_SUBMODULES="${RECURSE_SUBMODULES}" -CLONE_JOBS="${CLONE_JOBS}" +EXTRA_ARGS_B64="${EXTRA_ARGS}" POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}" PRE_CLONE_SCRIPT="${PRE_CLONE_SCRIPT}" @@ -50,14 +48,10 @@ fi # Build optional git clone flags CLONE_FLAGS=() -if [ "$DEPTH" -gt 0 ]; then - CLONE_FLAGS+=(--depth "$DEPTH") -fi -if [ "$RECURSE_SUBMODULES" = "true" ]; then - CLONE_FLAGS+=(--recurse-submodules) -fi -if [ "$CLONE_JOBS" -gt 0 ]; then - CLONE_FLAGS+=(--jobs "$CLONE_JOBS") +if [ -n "$EXTRA_ARGS_B64" ]; then + while IFS= read -r arg || [ -n "$arg" ]; do + [ -n "$arg" ] && CLONE_FLAGS+=("$arg") + done < <(echo "$EXTRA_ARGS_B64" | base64 -d) fi # Check if the directory is empty From cf98844e273cfc498db8af55562de1fc61e93428 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 19 May 2026 10:06:04 +0000 Subject: [PATCH 05/13] chore: address coder-agents-comments --- registry/coder/modules/git-clone/README.md | 7 +- registry/coder/modules/git-clone/main.test.ts | 81 ++++++++++++++++--- registry/coder/modules/git-clone/main.tf | 3 +- registry/coder/modules/git-clone/run.sh | 18 ++--- 4 files changed, 88 insertions(+), 21 deletions(-) diff --git a/registry/coder/modules/git-clone/README.md b/registry/coder/modules/git-clone/README.md index 2d3a1a827..513ba5119 100644 --- a/registry/coder/modules/git-clone/README.md +++ b/registry/coder/modules/git-clone/README.md @@ -189,9 +189,14 @@ module "git-clone" { Pass any additional flags through `extra_args` (one element per argument). This lets you enable anything `git clone` supports without the module having -to expose it explicitly — for example a shallow clone, submodules, parallel +to expose it explicitly, for example a shallow clone, submodules, parallel fetches, or partial clones. +> Do not put secrets in `extra_args`. The resolved `git clone` command +> (including every element of `extra_args`) is echoed to the workspace +> startup log, so values like `--config=http.extraHeader=Authorization: Bearer ` +> would appear there in plaintext. + ```tf module "git-clone" { count = data.coder_workspace.me.start_count diff --git a/registry/coder/modules/git-clone/main.test.ts b/registry/coder/modules/git-clone/main.test.ts index 93eaa32a5..e3175ed69 100644 --- a/registry/coder/modules/git-clone/main.test.ts +++ b/registry/coder/modules/git-clone/main.test.ts @@ -29,6 +29,20 @@ const executeScriptInContainer = async ( }; }; +// Drops a fake `git` onto PATH that prints each argv entry on its own line. +// Lets tests prove that arguments (including ones with embedded spaces) reach +// `git clone` as single argv tokens, which the echo line cannot show because +// it joins with spaces. +const installFakeGit = [ + "cat > /usr/local/bin/git <<'SHIM'", + "#!/bin/sh", + 'for arg in "$@"; do', + ' printf "argv:%s\\n" "$arg"', + "done", + "SHIM", + "chmod +x /usr/local/bin/git", +].join("\n"); + describe("git-clone", async () => { await runTerraformInit(import.meta.dir); @@ -56,7 +70,7 @@ describe("git-clone", async () => { expect(output.stdout).toEqual([ "Creating directory /root/fake-url...", "Cloning fake-url to /root/fake-url...", - "Running: git clone fake-url /root/fake-url", + "Running: git clone fake-url /root/fake-url", ]); expect(output.stderr.join(" ")).toContain("fatal"); expect(output.stderr.join(" ")).toContain("fake-url"); @@ -233,7 +247,7 @@ describe("git-clone", async () => { expect(output.stdout).toEqual([ "Creating directory /root/repo-tests.log...", "Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...", - "Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log", + "Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log", ]); }); @@ -247,7 +261,7 @@ describe("git-clone", async () => { expect(output.stdout).toEqual([ "Creating directory /root/repo-tests.log...", "Cloning https://gitlab.com/mike.brew/repo-tests.log to /root/repo-tests.log on branch feat/branch...", - "Running: git clone -b feat/branch https://gitlab.com/mike.brew/repo-tests.log /root/repo-tests.log", + "Running: git clone -b feat/branch https://gitlab.com/mike.brew/repo-tests.log /root/repo-tests.log", ]); }); @@ -269,7 +283,7 @@ describe("git-clone", async () => { expect(output.stdout).toEqual([ "Creating directory /root/repo-tests.log...", "Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...", - "Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log", + "Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log", ]); }); @@ -322,18 +336,67 @@ describe("git-clone", async () => { url: "fake-url", }); const script = findResourceInstance(state, "coder_script").script; - expect(script).toContain('EXTRA_ARGS_B64=""'); + expect(script).toContain('EXTRA_ARGS=""'); }); it("passes extra_args to git clone", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", url: "fake-url", - extra_args: '["--recurse-submodules", "--jobs=8"]', + extra_args: JSON.stringify([ + "--recurse-submodules", + "--jobs=8", + "--config=user.name=Coder User", + "-c", + "core.sshCommand=ssh -i /tmp/key", + ]), }); - const output = await executeScriptInContainer(state, "alpine/git"); - expect(output.stdout).toContain( - "Running: git clone --recurse-submodules --jobs=8 fake-url /root/fake-url", + const output = await executeScriptInContainer( + state, + "alpine/git", + installFakeGit, + ); + expect(output.exitCode).toBe(0); + expect(output.stdout.join("\n")).toContain( + [ + "argv:clone", + "argv:--recurse-submodules", + "argv:--jobs=8", + "argv:--config=user.name=Coder User", + "argv:-c", + "argv:core.sshCommand=ssh -i /tmp/key", + "argv:fake-url", + "argv:/root/fake-url", + ].join("\n"), + ); + }); + + it("passes extra_args alongside branch_name in the correct order", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "fake-url", + branch_name: "feat/branch", + extra_args: JSON.stringify([ + "--recurse-submodules", + "--config=user.name=Coder User", + ]), + }); + const output = await executeScriptInContainer( + state, + "alpine/git", + installFakeGit, + ); + expect(output.exitCode).toBe(0); + expect(output.stdout.join("\n")).toContain( + [ + "argv:clone", + "argv:--recurse-submodules", + "argv:--config=user.name=Coder User", + "argv:-b", + "argv:feat/branch", + "argv:fake-url", + "argv:/root/fake-url", + ].join("\n"), ); }); diff --git a/registry/coder/modules/git-clone/main.tf b/registry/coder/modules/git-clone/main.tf index 2a2f2e722..ab2279fef 100644 --- a/registry/coder/modules/git-clone/main.tf +++ b/registry/coder/modules/git-clone/main.tf @@ -97,8 +97,7 @@ locals { encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : "" # Encode the pre_clone_script for passing to the shell script encoded_pre_clone_script = var.pre_clone_script != null ? base64encode(var.pre_clone_script) : "" - # Encode extra clone args (newline-separated) so the shell script can split them into an array safely - encoded_extra_args = base64encode(join("\n", var.extra_args)) + encoded_extra_args = base64encode(join("\n", var.extra_args)) } output "repo_dir" { diff --git a/registry/coder/modules/git-clone/run.sh b/registry/coder/modules/git-clone/run.sh index 752e84bc2..bad790f83 100644 --- a/registry/coder/modules/git-clone/run.sh +++ b/registry/coder/modules/git-clone/run.sh @@ -7,7 +7,7 @@ CLONE_PATH="${CLONE_PATH}" BRANCH_NAME="${BRANCH_NAME}" # Expand home if it's specified! CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}" -EXTRA_ARGS_B64="${EXTRA_ARGS}" +EXTRA_ARGS="${EXTRA_ARGS}" POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}" PRE_CLONE_SCRIPT="${PRE_CLONE_SCRIPT}" @@ -47,11 +47,11 @@ if [ -n "$PRE_CLONE_SCRIPT" ]; then fi # Build optional git clone flags -CLONE_FLAGS=() -if [ -n "$EXTRA_ARGS_B64" ]; then +extra_args=() +if [ -n "$EXTRA_ARGS" ]; then while IFS= read -r arg || [ -n "$arg" ]; do - [ -n "$arg" ] && CLONE_FLAGS+=("$arg") - done < <(echo "$EXTRA_ARGS_B64" | base64 -d) + [ -n "$arg" ] && extra_args+=("$arg") + done < <(echo "$EXTRA_ARGS" | base64 -d) fi # Check if the directory is empty @@ -59,12 +59,12 @@ fi if [ -z "$(ls -A "$CLONE_PATH")" ]; then if [ -z "$BRANCH_NAME" ]; then echo "Cloning $REPO_URL to $CLONE_PATH..." - echo "Running: git clone $${CLONE_FLAGS[*]} $REPO_URL $CLONE_PATH" - git clone "$${CLONE_FLAGS[@]}" "$REPO_URL" "$CLONE_PATH" + echo "Running: git clone $${extra_args[@]:+$${extra_args[@]} }$REPO_URL $CLONE_PATH" + git clone $${extra_args[@]+"$${extra_args[@]}"} "$REPO_URL" "$CLONE_PATH" else echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..." - echo "Running: git clone $${CLONE_FLAGS[*]} -b $BRANCH_NAME $REPO_URL $CLONE_PATH" - git clone "$${CLONE_FLAGS[@]}" -b "$BRANCH_NAME" "$REPO_URL" "$CLONE_PATH" + echo "Running: git clone $${extra_args[@]:+$${extra_args[@]} }-b $BRANCH_NAME $REPO_URL $CLONE_PATH" + git clone $${extra_args[@]+"$${extra_args[@]}"} -b "$BRANCH_NAME" "$REPO_URL" "$CLONE_PATH" fi else echo "$CLONE_PATH already exists and isn't empty, skipping clone!" From 525c27d4e6b03c7e2cdefbd0da62238db315d783 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 25 May 2026 13:27:52 +0000 Subject: [PATCH 06/13] feat(git-clone): refactor pre and post-clone script handling with dedicated script paths and logging --- registry/coder/modules/git-clone/main.test.ts | 59 +++++++++++++++---- registry/coder/modules/git-clone/main.tf | 41 ++++++++++--- registry/coder/modules/git-clone/run.sh | 19 +++--- 3 files changed, 87 insertions(+), 32 deletions(-) diff --git a/registry/coder/modules/git-clone/main.test.ts b/registry/coder/modules/git-clone/main.test.ts index e3175ed69..39f1fdfe0 100644 --- a/registry/coder/modules/git-clone/main.test.ts +++ b/registry/coder/modules/git-clone/main.test.ts @@ -67,13 +67,13 @@ describe("git-clone", async () => { url: "fake-url", }); const output = await executeScriptInContainer(state, "alpine/git"); - expect(output.stdout).toEqual([ - "Creating directory /root/fake-url...", - "Cloning fake-url to /root/fake-url...", + expect(output.stdout).toContain("Creating directory /root/fake-url..."); + expect(output.stdout).toContain("Cloning fake-url to /root/fake-url..."); + expect(output.stdout).toContain( "Running: git clone fake-url /root/fake-url", - ]); - expect(output.stderr.join(" ")).toContain("fatal"); - expect(output.stderr.join(" ")).toContain("fake-url"); + ); + expect(output.stdout.join(" ")).toContain("fatal"); + expect(output.stdout.join(" ")).toContain("fake-url"); }); it("repo_dir should match repo name for https", async () => { @@ -244,11 +244,15 @@ describe("git-clone", async () => { }); const output = await executeScriptInContainer(state, "alpine/git"); expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ + expect(output.stdout).toContain( "Creating directory /root/repo-tests.log...", + ); + expect(output.stdout).toContain( "Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...", + ); + expect(output.stdout).toContain( "Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log", - ]); + ); }); it("runs with gitlab clone with switch to feat/branch", async () => { @@ -258,11 +262,15 @@ describe("git-clone", async () => { }); const output = await executeScriptInContainer(state, "alpine/git"); expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ + expect(output.stdout).toContain( "Creating directory /root/repo-tests.log...", + ); + expect(output.stdout).toContain( "Cloning https://gitlab.com/mike.brew/repo-tests.log to /root/repo-tests.log on branch feat/branch...", + ); + expect(output.stdout).toContain( "Running: git clone -b feat/branch https://gitlab.com/mike.brew/repo-tests.log /root/repo-tests.log", - ]); + ); }); it("runs with github clone with branch_name set to feat/branch", async () => { @@ -280,11 +288,15 @@ describe("git-clone", async () => { const output = await executeScriptInContainer(state, "alpine/git"); expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ + expect(output.stdout).toContain( "Creating directory /root/repo-tests.log...", + ); + expect(output.stdout).toContain( "Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...", + ); + expect(output.stdout).toContain( "Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log", - ]); + ); }); it("runs post-clone script", async () => { @@ -336,7 +348,10 @@ describe("git-clone", async () => { url: "fake-url", }); const script = findResourceInstance(state, "coder_script").script; - expect(script).toContain('EXTRA_ARGS=""'); + const match = script.match(/echo -n '([^']+)'/); + expect(match).not.toBeNull(); + const cloneScript = Buffer.from(match![1], "base64").toString(); + expect(cloneScript).toContain('EXTRA_ARGS=""'); }); it("passes extra_args to git clone", async () => { @@ -400,6 +415,24 @@ describe("git-clone", async () => { ); }); + it("writes output to logs/clone.log under module directory", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "fake-url", + }); + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer("alpine/git"); + await execContainer(id, ["sh", "-c", "apk add --no-cache bash >/dev/null"]); + await execContainer(id, ["bash", "-c", instance.script]); + const log = await execContainer(id, [ + "cat", + "/root/.coder-modules/coder/git-clone/logs/clone.log", + ]); + expect(log.exitCode).toBe(0); + expect(log.stdout).toContain("Cloning fake-url to /root/fake-url..."); + expect(log.stdout).toContain("Running: git clone fake-url /root/fake-url"); + }); + it("fails when post-clone script fails", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", diff --git a/registry/coder/modules/git-clone/main.tf b/registry/coder/modules/git-clone/main.tf index ab2279fef..20d2675da 100644 --- a/registry/coder/modules/git-clone/main.tf +++ b/registry/coder/modules/git-clone/main.tf @@ -98,6 +98,23 @@ locals { # Encode the pre_clone_script for passing to the shell script encoded_pre_clone_script = var.pre_clone_script != null ? base64encode(var.pre_clone_script) : "" encoded_extra_args = base64encode(join("\n", var.extra_args)) + + # Module directory paths (matches coder-utils convention) + module_dir = "$HOME/.coder-modules/coder/git-clone" + scripts_directory = "${local.module_dir}/scripts" + log_directory = "${local.module_dir}/logs" + clone_script_path = "${local.scripts_directory}/clone.sh" + clone_log_path = "${local.log_directory}/clone.log" + + encoded_clone_script = base64encode(templatefile("${path.module}/run.sh", { + CLONE_PATH = local.clone_path, + REPO_URL = local.clone_url, + BRANCH_NAME = local.branch_name, + EXTRA_ARGS = local.encoded_extra_args, + POST_CLONE_SCRIPT = local.encoded_post_clone_script, + PRE_CLONE_SCRIPT = local.encoded_pre_clone_script, + SCRIPTS_DIR = local.scripts_directory, + })) } output "repo_dir" { @@ -131,15 +148,21 @@ output "branch_name" { } resource "coder_script" "git_clone" { - agent_id = var.agent_id - script = templatefile("${path.module}/run.sh", { - CLONE_PATH = local.clone_path, - REPO_URL : local.clone_url, - BRANCH_NAME : local.branch_name, - EXTRA_ARGS = local.encoded_extra_args, - POST_CLONE_SCRIPT : local.encoded_post_clone_script, - PRE_CLONE_SCRIPT : local.encoded_pre_clone_script, - }) + agent_id = var.agent_id + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + mkdir -p ${local.module_dir} + mkdir -p ${local.scripts_directory} + mkdir -p ${local.log_directory} + + echo -n '${local.encoded_clone_script}' | base64 -d > ${local.clone_script_path} + chmod +x ${local.clone_script_path} + + ${local.clone_script_path} 2>&1 | tee ${local.clone_log_path} + EOT display_name = "Git Clone" icon = "/icon/git.svg" run_on_start = true diff --git a/registry/coder/modules/git-clone/run.sh b/registry/coder/modules/git-clone/run.sh index bad790f83..7cba33069 100644 --- a/registry/coder/modules/git-clone/run.sh +++ b/registry/coder/modules/git-clone/run.sh @@ -10,6 +10,7 @@ CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}" EXTRA_ARGS="${EXTRA_ARGS}" POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}" PRE_CLONE_SCRIPT="${PRE_CLONE_SCRIPT}" +SCRIPTS_DIR="${SCRIPTS_DIR}" # Check if the variable is empty... if [ -z "$REPO_URL" ]; then @@ -39,11 +40,10 @@ fi # Run pre-clone script if provided if [ -n "$PRE_CLONE_SCRIPT" ]; then echo "Running pre-clone script..." - PRE_CLONE_TMP=$(mktemp) - echo "$PRE_CLONE_SCRIPT" | base64 -d > "$PRE_CLONE_TMP" - chmod +x "$PRE_CLONE_TMP" - $PRE_CLONE_TMP - rm "$PRE_CLONE_TMP" + PRE_CLONE_PATH="$SCRIPTS_DIR/pre_clone.sh" + echo "$PRE_CLONE_SCRIPT" | base64 -d > "$PRE_CLONE_PATH" + chmod +x "$PRE_CLONE_PATH" + "$PRE_CLONE_PATH" fi # Build optional git clone flags @@ -73,10 +73,9 @@ fi # Run post-clone script if provided if [ -n "$POST_CLONE_SCRIPT" ]; then echo "Running post-clone script..." - POST_CLONE_TMP=$(mktemp) - echo "$POST_CLONE_SCRIPT" | base64 -d > "$POST_CLONE_TMP" - chmod +x "$POST_CLONE_TMP" + POST_CLONE_PATH="$SCRIPTS_DIR/post_clone.sh" + echo "$POST_CLONE_SCRIPT" | base64 -d > "$POST_CLONE_PATH" + chmod +x "$POST_CLONE_PATH" cd "$CLONE_PATH" || exit - $POST_CLONE_TMP - rm "$POST_CLONE_TMP" + "$POST_CLONE_PATH" fi From e679ef8646988d0373c429ebb4c4160e9fccef95 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 25 May 2026 13:36:16 +0000 Subject: [PATCH 07/13] feat(git-clone): update version from 1.4.0 to 2.0.0 across all modules --- registry/coder/modules/git-clone/README.md | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/registry/coder/modules/git-clone/README.md b/registry/coder/modules/git-clone/README.md index 513ba5119..69e6bddce 100644 --- a/registry/coder/modules/git-clone/README.md +++ b/registry/coder/modules/git-clone/README.md @@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" } @@ -28,7 +28,7 @@ module "git-clone" { module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" base_dir = "~/projects/coder" @@ -43,7 +43,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" } @@ -70,7 +70,7 @@ data "coder_parameter" "git_repo" { module "git_clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = data.coder_parameter.git_repo.value } @@ -105,7 +105,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = "https://github.example.com/coder/coder/tree/feat/example" git_providers = { @@ -125,7 +125,7 @@ To GitLab clone with a specific branch like `feat/example` module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = "https://gitlab.com/coder/coder/-/tree/feat/example" } @@ -137,7 +137,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com` module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = "https://gitlab.example.com/coder/coder/-/tree/feat/example" git_providers = { @@ -159,7 +159,7 @@ For example, to clone the `feat/example` branch: module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" branch_name = "feat/example" @@ -177,7 +177,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder: module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" folder_name = "coder-dev" @@ -201,7 +201,7 @@ fetches, or partial clones. module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" extra_args = [ @@ -222,7 +222,7 @@ This is useful for preparing the environment or validating prerequisites before module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" pre_clone_script = <<-EOT @@ -245,7 +245,7 @@ This is useful for running initialization tasks like installing dependencies or module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "1.4.0" + version = "2.0.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" post_clone_script = <<-EOT From d86df7597b741565d82437a1065c6363acd8c0d1 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 25 May 2026 13:52:17 +0000 Subject: [PATCH 08/13] feat(git-clone): add pre/post clone log paths matching coder-utils convention - Add pre_clone_log_path and post_clone_log_path locals in main.tf - Pass log paths to run.sh as template variables - Pipe pre/post clone script execution through tee to dedicated log files --- registry/coder/modules/git-clone/main.tf | 22 +++++++++++++--------- registry/coder/modules/git-clone/run.sh | 6 ++++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/registry/coder/modules/git-clone/main.tf b/registry/coder/modules/git-clone/main.tf index 20d2675da..1a7bd65d8 100644 --- a/registry/coder/modules/git-clone/main.tf +++ b/registry/coder/modules/git-clone/main.tf @@ -103,17 +103,21 @@ locals { module_dir = "$HOME/.coder-modules/coder/git-clone" scripts_directory = "${local.module_dir}/scripts" log_directory = "${local.module_dir}/logs" - clone_script_path = "${local.scripts_directory}/clone.sh" - clone_log_path = "${local.log_directory}/clone.log" + clone_script_path = "${local.scripts_directory}/clone.sh" + clone_log_path = "${local.log_directory}/clone.log" + pre_clone_log_path = "${local.log_directory}/pre_clone.log" + post_clone_log_path = "${local.log_directory}/post_clone.log" encoded_clone_script = base64encode(templatefile("${path.module}/run.sh", { - CLONE_PATH = local.clone_path, - REPO_URL = local.clone_url, - BRANCH_NAME = local.branch_name, - EXTRA_ARGS = local.encoded_extra_args, - POST_CLONE_SCRIPT = local.encoded_post_clone_script, - PRE_CLONE_SCRIPT = local.encoded_pre_clone_script, - SCRIPTS_DIR = local.scripts_directory, + CLONE_PATH = local.clone_path, + REPO_URL = local.clone_url, + BRANCH_NAME = local.branch_name, + EXTRA_ARGS = local.encoded_extra_args, + POST_CLONE_SCRIPT = local.encoded_post_clone_script, + PRE_CLONE_SCRIPT = local.encoded_pre_clone_script, + SCRIPTS_DIR = local.scripts_directory, + PRE_CLONE_LOG_PATH = local.pre_clone_log_path, + POST_CLONE_LOG_PATH = local.post_clone_log_path, })) } diff --git a/registry/coder/modules/git-clone/run.sh b/registry/coder/modules/git-clone/run.sh index 7cba33069..59e28d173 100644 --- a/registry/coder/modules/git-clone/run.sh +++ b/registry/coder/modules/git-clone/run.sh @@ -11,6 +11,8 @@ EXTRA_ARGS="${EXTRA_ARGS}" POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}" PRE_CLONE_SCRIPT="${PRE_CLONE_SCRIPT}" SCRIPTS_DIR="${SCRIPTS_DIR}" +PRE_CLONE_LOG_PATH="${PRE_CLONE_LOG_PATH}" +POST_CLONE_LOG_PATH="${POST_CLONE_LOG_PATH}" # Check if the variable is empty... if [ -z "$REPO_URL" ]; then @@ -43,7 +45,7 @@ if [ -n "$PRE_CLONE_SCRIPT" ]; then PRE_CLONE_PATH="$SCRIPTS_DIR/pre_clone.sh" echo "$PRE_CLONE_SCRIPT" | base64 -d > "$PRE_CLONE_PATH" chmod +x "$PRE_CLONE_PATH" - "$PRE_CLONE_PATH" + "$PRE_CLONE_PATH" 2>&1 | tee "$PRE_CLONE_LOG_PATH" fi # Build optional git clone flags @@ -77,5 +79,5 @@ if [ -n "$POST_CLONE_SCRIPT" ]; then echo "$POST_CLONE_SCRIPT" | base64 -d > "$POST_CLONE_PATH" chmod +x "$POST_CLONE_PATH" cd "$CLONE_PATH" || exit - "$POST_CLONE_PATH" + "$POST_CLONE_PATH" 2>&1 | tee "$POST_CLONE_LOG_PATH" fi From 295724a75d12e2b585782856ab5ddfef4a0fcd5f Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 25 May 2026 13:55:28 +0000 Subject: [PATCH 09/13] bun fmt --- registry/coder/modules/git-clone/main.tf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/registry/coder/modules/git-clone/main.tf b/registry/coder/modules/git-clone/main.tf index 1a7bd65d8..1b6206a9a 100644 --- a/registry/coder/modules/git-clone/main.tf +++ b/registry/coder/modules/git-clone/main.tf @@ -100,11 +100,11 @@ locals { encoded_extra_args = base64encode(join("\n", var.extra_args)) # Module directory paths (matches coder-utils convention) - module_dir = "$HOME/.coder-modules/coder/git-clone" - scripts_directory = "${local.module_dir}/scripts" - log_directory = "${local.module_dir}/logs" - clone_script_path = "${local.scripts_directory}/clone.sh" - clone_log_path = "${local.log_directory}/clone.log" + module_dir = "$HOME/.coder-modules/coder/git-clone" + scripts_directory = "${local.module_dir}/scripts" + log_directory = "${local.module_dir}/logs" + clone_script_path = "${local.scripts_directory}/clone.sh" + clone_log_path = "${local.log_directory}/clone.log" pre_clone_log_path = "${local.log_directory}/pre_clone.log" post_clone_log_path = "${local.log_directory}/post_clone.log" From 9bf3bb9ff800a635d06827a7e799c42169b3389c Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Tue, 26 May 2026 11:04:25 +0000 Subject: [PATCH 10/13] fix(coder/modules/git-clone): address review findings from R4 - Add migration guidance for depth -> extra_args (DEREM-7) - Use folder_name in module_dir to avoid multi-instance collisions (CRF-1) - Remove diagnostic 'Running:' echo lines (CRF-2) - Add Troubleshooting section documenting log paths (CRF-3) - Use robust base64 regex in test (CRF-4) - Add exit code assertion to 'runs with git' test (CRF-5) --- registry/coder/modules/git-clone/README.md | 13 +++++++++++ registry/coder/modules/git-clone/main.test.ts | 22 +++++-------------- registry/coder/modules/git-clone/main.tf | 4 +++- registry/coder/modules/git-clone/run.sh | 2 -- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/registry/coder/modules/git-clone/README.md b/registry/coder/modules/git-clone/README.md index 69e6bddce..b1253baf9 100644 --- a/registry/coder/modules/git-clone/README.md +++ b/registry/coder/modules/git-clone/README.md @@ -187,6 +187,8 @@ module "git-clone" { ## Extra `git clone` arguments +> **Upgrading from v1.x?** The `depth` variable was removed in v2.0.0. Use `extra_args = ["--depth=1"]` instead. + Pass any additional flags through `extra_args` (one element per argument). This lets you enable anything `git clone` supports without the module having to expose it explicitly, for example a shallow clone, submodules, parallel @@ -258,3 +260,14 @@ module "git-clone" { EOT } ``` + +## Troubleshooting + +Clone output is logged to `~/.coder-modules/coder/git-clone//logs/clone.log`: + + cat ~/.coder-modules/coder/git-clone/*/logs/clone.log + +Pre-clone and post-clone script output is logged alongside: + + cat ~/.coder-modules/coder/git-clone/*/logs/pre_clone.log + cat ~/.coder-modules/coder/git-clone/*/logs/post_clone.log diff --git a/registry/coder/modules/git-clone/main.test.ts b/registry/coder/modules/git-clone/main.test.ts index 39f1fdfe0..feaec4b27 100644 --- a/registry/coder/modules/git-clone/main.test.ts +++ b/registry/coder/modules/git-clone/main.test.ts @@ -69,9 +69,7 @@ describe("git-clone", async () => { const output = await executeScriptInContainer(state, "alpine/git"); expect(output.stdout).toContain("Creating directory /root/fake-url..."); expect(output.stdout).toContain("Cloning fake-url to /root/fake-url..."); - expect(output.stdout).toContain( - "Running: git clone fake-url /root/fake-url", - ); + expect(output.exitCode).not.toBe(0); expect(output.stdout.join(" ")).toContain("fatal"); expect(output.stdout.join(" ")).toContain("fake-url"); }); @@ -250,9 +248,6 @@ describe("git-clone", async () => { expect(output.stdout).toContain( "Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...", ); - expect(output.stdout).toContain( - "Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log", - ); }); it("runs with gitlab clone with switch to feat/branch", async () => { @@ -268,9 +263,6 @@ describe("git-clone", async () => { expect(output.stdout).toContain( "Cloning https://gitlab.com/mike.brew/repo-tests.log to /root/repo-tests.log on branch feat/branch...", ); - expect(output.stdout).toContain( - "Running: git clone -b feat/branch https://gitlab.com/mike.brew/repo-tests.log /root/repo-tests.log", - ); }); it("runs with github clone with branch_name set to feat/branch", async () => { @@ -294,9 +286,6 @@ describe("git-clone", async () => { expect(output.stdout).toContain( "Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...", ); - expect(output.stdout).toContain( - "Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log", - ); }); it("runs post-clone script", async () => { @@ -348,7 +337,7 @@ describe("git-clone", async () => { url: "fake-url", }); const script = findResourceInstance(state, "coder_script").script; - const match = script.match(/echo -n '([^']+)'/); + const match = script.match(/'([A-Za-z0-9+/=]+)'\s*\|\s*base64\s+-d/); expect(match).not.toBeNull(); const cloneScript = Buffer.from(match![1], "base64").toString(); expect(cloneScript).toContain('EXTRA_ARGS=""'); @@ -425,12 +414,13 @@ describe("git-clone", async () => { await execContainer(id, ["sh", "-c", "apk add --no-cache bash >/dev/null"]); await execContainer(id, ["bash", "-c", instance.script]); const log = await execContainer(id, [ - "cat", - "/root/.coder-modules/coder/git-clone/logs/clone.log", + "bash", + "-c", + "cat /root/.coder-modules/coder/git-clone/*/logs/clone.log", ]); expect(log.exitCode).toBe(0); expect(log.stdout).toContain("Cloning fake-url to /root/fake-url..."); - expect(log.stdout).toContain("Running: git clone fake-url /root/fake-url"); + }); it("fails when post-clone script fails", async () => { diff --git a/registry/coder/modules/git-clone/main.tf b/registry/coder/modules/git-clone/main.tf index 1b6206a9a..22a97d407 100644 --- a/registry/coder/modules/git-clone/main.tf +++ b/registry/coder/modules/git-clone/main.tf @@ -100,7 +100,9 @@ locals { encoded_extra_args = base64encode(join("\n", var.extra_args)) # Module directory paths (matches coder-utils convention) - module_dir = "$HOME/.coder-modules/coder/git-clone" + # Use folder_name so two git-clone instances in the same template get + # separate script and log directories. + module_dir = "$HOME/.coder-modules/coder/git-clone/${local.folder_name}" scripts_directory = "${local.module_dir}/scripts" log_directory = "${local.module_dir}/logs" clone_script_path = "${local.scripts_directory}/clone.sh" diff --git a/registry/coder/modules/git-clone/run.sh b/registry/coder/modules/git-clone/run.sh index 59e28d173..fb2456af5 100644 --- a/registry/coder/modules/git-clone/run.sh +++ b/registry/coder/modules/git-clone/run.sh @@ -61,11 +61,9 @@ fi if [ -z "$(ls -A "$CLONE_PATH")" ]; then if [ -z "$BRANCH_NAME" ]; then echo "Cloning $REPO_URL to $CLONE_PATH..." - echo "Running: git clone $${extra_args[@]:+$${extra_args[@]} }$REPO_URL $CLONE_PATH" git clone $${extra_args[@]+"$${extra_args[@]}"} "$REPO_URL" "$CLONE_PATH" else echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..." - echo "Running: git clone $${extra_args[@]:+$${extra_args[@]} }-b $BRANCH_NAME $REPO_URL $CLONE_PATH" git clone $${extra_args[@]+"$${extra_args[@]}"} -b "$BRANCH_NAME" "$REPO_URL" "$CLONE_PATH" fi else From fcbb5889ac47063565409037f85ebffd46907eea Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Tue, 26 May 2026 16:09:27 +0000 Subject: [PATCH 11/13] fix(coder/modules/git-clone): quote heredoc paths, update stale docs, improve test - Quote all path interpolations in coder_script heredoc to handle spaces and shell metacharacters in folder_name - Update README secrets warning to cite actual exposure vectors (script on disk, Terraform state) after removing echo lines - Replace mechanism-coupled extra_args test with behavioral check using the fake-git shim - Remove stray blank line in log test - Add note about -b/--branch conflict with branch_name --- registry/coder/modules/git-clone/README.md | 13 +++++++++---- registry/coder/modules/git-clone/main.test.ts | 15 +++++++++------ registry/coder/modules/git-clone/main.tf | 12 ++++++------ 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/registry/coder/modules/git-clone/README.md b/registry/coder/modules/git-clone/README.md index b1253baf9..cb61b8e2a 100644 --- a/registry/coder/modules/git-clone/README.md +++ b/registry/coder/modules/git-clone/README.md @@ -194,10 +194,15 @@ This lets you enable anything `git clone` supports without the module having to expose it explicitly, for example a shallow clone, submodules, parallel fetches, or partial clones. -> Do not put secrets in `extra_args`. The resolved `git clone` command -> (including every element of `extra_args`) is echoed to the workspace -> startup log, so values like `--config=http.extraHeader=Authorization: Bearer ` -> would appear there in plaintext. +> Do not pass `-b` or `--branch` in `extra_args` when `branch_name` is +> already set (or extracted from the URL). Git silently accepts the last +> `-b` flag, so the two values would conflict. + +> Do not put secrets in `extra_args`. The values are embedded in the +> clone script on disk (`~/.coder-modules/.../scripts/clone.sh`) and +> stored in Terraform state, so values like +> `--config=http.extraHeader=Authorization: Bearer ` would be +> readable in plaintext. ```tf module "git-clone" { diff --git a/registry/coder/modules/git-clone/main.test.ts b/registry/coder/modules/git-clone/main.test.ts index feaec4b27..8ac2925bd 100644 --- a/registry/coder/modules/git-clone/main.test.ts +++ b/registry/coder/modules/git-clone/main.test.ts @@ -336,11 +336,15 @@ describe("git-clone", async () => { agent_id: "foo", url: "fake-url", }); - const script = findResourceInstance(state, "coder_script").script; - const match = script.match(/'([A-Za-z0-9+/=]+)'\s*\|\s*base64\s+-d/); - expect(match).not.toBeNull(); - const cloneScript = Buffer.from(match![1], "base64").toString(); - expect(cloneScript).toContain('EXTRA_ARGS=""'); + const output = await executeScriptInContainer( + state, + "alpine/git", + installFakeGit, + ); + // With no extra_args the only argv tokens should be clone, url, path. + expect(output.stdout.join("\n")).toContain( + ["argv:clone", "argv:fake-url", "argv:/root/fake-url"].join("\n"), + ); }); it("passes extra_args to git clone", async () => { @@ -420,7 +424,6 @@ describe("git-clone", async () => { ]); expect(log.exitCode).toBe(0); expect(log.stdout).toContain("Cloning fake-url to /root/fake-url..."); - }); it("fails when post-clone script fails", async () => { diff --git a/registry/coder/modules/git-clone/main.tf b/registry/coder/modules/git-clone/main.tf index 22a97d407..b4e6a4f8c 100644 --- a/registry/coder/modules/git-clone/main.tf +++ b/registry/coder/modules/git-clone/main.tf @@ -160,14 +160,14 @@ resource "coder_script" "git_clone" { set -o errexit set -o pipefail - mkdir -p ${local.module_dir} - mkdir -p ${local.scripts_directory} - mkdir -p ${local.log_directory} + mkdir -p "${local.module_dir}" + mkdir -p "${local.scripts_directory}" + mkdir -p "${local.log_directory}" - echo -n '${local.encoded_clone_script}' | base64 -d > ${local.clone_script_path} - chmod +x ${local.clone_script_path} + echo -n '${local.encoded_clone_script}' | base64 -d > "${local.clone_script_path}" + chmod +x "${local.clone_script_path}" - ${local.clone_script_path} 2>&1 | tee ${local.clone_log_path} + "${local.clone_script_path}" 2>&1 | tee "${local.clone_log_path}" EOT display_name = "Git Clone" icon = "/icon/git.svg" From f3a1f4dea83d376de6401bf5986d94cb22a095ec Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Tue, 26 May 2026 16:33:07 +0000 Subject: [PATCH 12/13] docs(coder/modules/git-clone): use GFM alert syntax for blockquotes --- registry/coder/modules/git-clone/README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/registry/coder/modules/git-clone/README.md b/registry/coder/modules/git-clone/README.md index cb61b8e2a..e23b532b3 100644 --- a/registry/coder/modules/git-clone/README.md +++ b/registry/coder/modules/git-clone/README.md @@ -187,6 +187,7 @@ module "git-clone" { ## Extra `git clone` arguments +> [!NOTE] > **Upgrading from v1.x?** The `depth` variable was removed in v2.0.0. Use `extra_args = ["--depth=1"]` instead. Pass any additional flags through `extra_args` (one element per argument). @@ -194,15 +195,11 @@ This lets you enable anything `git clone` supports without the module having to expose it explicitly, for example a shallow clone, submodules, parallel fetches, or partial clones. +> [!WARNING] > Do not pass `-b` or `--branch` in `extra_args` when `branch_name` is > already set (or extracted from the URL). Git silently accepts the last > `-b` flag, so the two values would conflict. -> Do not put secrets in `extra_args`. The values are embedded in the -> clone script on disk (`~/.coder-modules/.../scripts/clone.sh`) and -> stored in Terraform state, so values like -> `--config=http.extraHeader=Authorization: Bearer ` would be -> readable in plaintext. ```tf module "git-clone" { From 216137636b2f03e8ea583506a8b14099dfd84f8a Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Tue, 26 May 2026 16:40:20 +0000 Subject: [PATCH 13/13] chore(coder/modules/git-clone): remove secrets warning, merge branch note into NOTE block --- registry/coder/modules/git-clone/README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/registry/coder/modules/git-clone/README.md b/registry/coder/modules/git-clone/README.md index e23b532b3..54bb5f0fd 100644 --- a/registry/coder/modules/git-clone/README.md +++ b/registry/coder/modules/git-clone/README.md @@ -189,18 +189,15 @@ module "git-clone" { > [!NOTE] > **Upgrading from v1.x?** The `depth` variable was removed in v2.0.0. Use `extra_args = ["--depth=1"]` instead. +> Do not pass `-b` or `--branch` in `extra_args` when `branch_name` is +> already set (or extracted from the URL). Git silently accepts the last +> `-b` flag, so the two values would conflict. Pass any additional flags through `extra_args` (one element per argument). This lets you enable anything `git clone` supports without the module having to expose it explicitly, for example a shallow clone, submodules, parallel fetches, or partial clones. -> [!WARNING] -> Do not pass `-b` or `--branch` in `extra_args` when `branch_name` is -> already set (or extracted from the URL). Git silently accepts the last -> `-b` flag, so the two values would conflict. - - ```tf module "git-clone" { count = data.coder_workspace.me.start_count