diff --git a/registry/coder/modules/git-clone/README.md b/registry/coder/modules/git-clone/README.md index 7fa594874..cb0fcec3f 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 = "2.0.5" + version = "2.1.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 = "2.0.5" + version = "2.1.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 = "2.0.5" + version = "2.1.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 = "2.0.5" + version = "2.1.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 = "2.0.5" + version = "2.1.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 = "2.0.5" + version = "2.1.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 = "2.0.5" + version = "2.1.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 = "2.0.5" + version = "2.1.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 = "2.0.5" + version = "2.1.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" folder_name = "coder-dev" @@ -188,32 +188,38 @@ 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. +> **Upgrading from v1.x?** The `depth` variable was removed in v2.0.0. Use `additional_args = ["--depth", "1"]` instead. +> `additional_args` rejects `-b` and `--branch`; set `branch_name` 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. +Pass any additional arguments through `additional_args`, a list with 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. Each element is passed +exactly as written and in order, so a flag can repeat and a value can contain +spaces. ```tf module "git-clone" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-clone/coder" - version = "2.0.5" + version = "2.1.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" - extra_args = [ - "--depth=1", + additional_args = [ + "--depth", "1", "--recurse-submodules", - "--jobs=8", - "--filter=blob:none", + "--jobs", "8", + "--filter", "blob:none", ] } ``` +`extra_args` is deprecated in favor of `additional_args`, which takes the same +list. It still works and is appended after `additional_args`. + ## Pre-clone script Run a custom script before cloning the repository by setting the `pre_clone_script` variable. @@ -223,7 +229,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 = "2.0.5" + version = "2.1.0" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" pre_clone_script = <<-EOT @@ -246,7 +252,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 = "2.0.5" + version = "2.1.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/git-clone.tftest.hcl b/registry/coder/modules/git-clone/git-clone.tftest.hcl index 7943bd6be..519ccb7c2 100644 --- a/registry/coder/modules/git-clone/git-clone.tftest.hcl +++ b/registry/coder/modules/git-clone/git-clone.tftest.hcl @@ -77,3 +77,44 @@ run "skips_clone_script_for_whitespace_url" { error_message = "folder_name and repo_dir should be empty when there is no repository and no folder name" } } + +run "deprecated_extra_args_still_applied" { + command = plan + + variables { + agent_id = "foo" + url = "https://github.com/coder/coder" + additional_args = ["--depth", "1", "-c", "http.extraHeader=X-A: 1", "-c", "http.extraHeader=X-B: 2"] + extra_args = ["--jobs=8"] + } + + # additional_args keeps its order and repeated flags, then extra_args follows. + assert { + condition = local.encoded_extra_args == base64encode("--depth\n1\n-c\nhttp.extraHeader=X-A: 1\n-c\nhttp.extraHeader=X-B: 2\n--jobs=8") + error_message = "extra_args must still be passed, after additional_args" + } +} + +run "additional_args_rejects_newline" { + command = plan + + variables { + agent_id = "foo" + url = "https://github.com/coder/coder" + additional_args = ["--depth\n1"] + } + + expect_failures = [var.additional_args] +} + +run "additional_args_rejects_branch" { + command = plan + + variables { + agent_id = "foo" + url = "https://github.com/coder/coder" + additional_args = ["--branch=main"] + } + + expect_failures = [var.additional_args] +} diff --git a/registry/coder/modules/git-clone/main.test.ts b/registry/coder/modules/git-clone/main.test.ts index d906f5ad5..dfad4d83f 100644 --- a/registry/coder/modules/git-clone/main.test.ts +++ b/registry/coder/modules/git-clone/main.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "bun:test"; +import { describe, expect, it, setDefaultTimeout } from "bun:test"; import { execContainer, findResourceInstance, @@ -10,6 +10,9 @@ import { type TerraformState, } from "~test"; +// The first test to use an image pulls it, which can outlast bun's 5s default. +setDefaultTimeout(60 * 1000); + const executeScriptInContainer = async ( state: TerraformState, image: string, @@ -417,6 +420,40 @@ describe("git-clone", async () => { ); }); + it("passes additional_args to git clone before extra_args", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "fake-url", + additional_args: JSON.stringify([ + "--recurse-submodules", + "-c", + "user.name=Coder User", + "-c", + "core.sshCommand=ssh -i /tmp/key", + ]), + extra_args: JSON.stringify(["--jobs=8"]), + }); + 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:-c", + "argv:user.name=Coder User", + "argv:-c", + "argv:core.sshCommand=ssh -i /tmp/key", + "argv:--jobs=8", + "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", diff --git a/registry/coder/modules/git-clone/main.tf b/registry/coder/modules/git-clone/main.tf index bf7146ecf..d9a32c7c7 100644 --- a/registry/coder/modules/git-clone/main.tf +++ b/registry/coder/modules/git-clone/main.tf @@ -56,8 +56,26 @@ variable "folder_name" { default = "" } +variable "additional_args" { + description = "Additional arguments to pass to `git clone`, one list element per argument, in order (e.g. `[\"--depth\", \"1\", \"--recurse-submodules\", \"-c\", \"http.extraHeader=X-A: 1\", \"-c\", \"http.extraHeader=X-B: 2\"]`). Each element is passed as written, so a flag can repeat and a value may contain spaces." + type = list(string) + default = [] + + validation { + condition = alltrue([for a in var.additional_args : a != "" && !can(regex("\n", a))]) + error_message = "additional_args elements must be non-empty and must not contain newlines." + } + + # The module already sets the branch from branch_name or the URL. Keep this + # list in sync with the `git clone` flags in run.sh. + validation { + condition = !anytrue([for a in var.additional_args : contains(["-b", "--branch"], split("=", a)[0])]) + error_message = "additional_args must not set -b or --branch; use branch_name instead." + } +} + variable "extra_args" { - description = "Extra arguments to pass to `git clone`, one element per argument (e.g. `[\"--recurse-submodules\", \"--jobs=8\", \"--filter=blob:none\"]`)." + description = "Deprecated: use `additional_args` instead, which takes the same list. Still honored and appended after `additional_args`." type = list(string) default = [] } @@ -99,7 +117,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) : "" - encoded_extra_args = base64encode(join("\n", var.extra_args)) + encoded_extra_args = base64encode(join("\n", concat(var.additional_args, var.extra_args))) # Module directory paths (matches coder-utils convention) # Use folder_name so two git-clone instances in the same template get diff --git a/registry/coder/modules/git-clone/run.sh b/registry/coder/modules/git-clone/run.sh index b9d2ff83a..9b6e5e1ca 100644 --- a/registry/coder/modules/git-clone/run.sh +++ b/registry/coder/modules/git-clone/run.sh @@ -118,6 +118,8 @@ if [ -z "$(ls -A "$CLONE_PATH")" ]; then git clone $${extra_args[@]+"$${extra_args[@]}"} "$REPO_URL" "$CLONE_PATH" else echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..." + # Keep -b in sync with the reserved-flag list in additional_args' + # validation in main.tf, so additional_args cannot set it a second time. git clone $${extra_args[@]+"$${extra_args[@]}"} -b "$BRANCH_NAME" "$REPO_URL" "$CLONE_PATH" fi else