From 960629762072deb407e5816e56481059dc19aaf9 Mon Sep 17 00:00:00 2001 From: "blinkagent[bot]" <237617714+blinkagent[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:11:19 -0500 Subject: [PATCH] feat: pass branch to coder dotfiles (#789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes #551 (fork branch couldn't be rebased due to GitHub App permission limitations). Original author: @willshu ## Description Adds support for specifying a git branch when cloning dotfiles repositories. ### Changes - Introduces `dotfiles_branch` and `default_dotfiles_branch` Terraform variables - Adds a `coder_parameter` for `dotfiles_branch` when not explicitly set (with `order` matching `dotfiles_uri`) - Conditionally passes the `--branch` flag to `coder dotfiles` only when branch is non-empty - Adds validation to prevent empty string for `dotfiles_branch` (use `null` to prompt the user) - Default branch is empty string — defers to the repo's default branch rather than assuming `main`, matching the behavior of `coder dotfiles --branch` which states: *"If empty, will default to cloning the default branch or using the existing branch in the cloned repo on disk."* - Adds test coverage for custom branch setting and parameter creation ### Review feedback addressed (from Copilot on #551) - Added `order` field to `dotfiles_branch` parameter for UI consistency with `dotfiles_uri` - Conditional echo message — only shows branch info when set - `--branch` flag only passed when `DOTFILES_BRANCH` is non-empty (both current-user and sudo paths) - Added validation block on `var.dotfiles_branch` to reject empty strings ## Type of Change - [x] Feature/enhancement ## Module Information **Path:** `registry/coder/modules/dotfiles` ## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun fmt`) - [ ] Changes tested locally Co-authored-by: William Shu Co-authored-by: DevCats --- registry/coder/modules/dotfiles/README.md | 12 +++---- registry/coder/modules/dotfiles/main.test.ts | 36 +++++++++++++++++++- registry/coder/modules/dotfiles/main.tf | 32 +++++++++++++++++ registry/coder/modules/dotfiles/run.sh | 19 +++++++++-- 4 files changed, 89 insertions(+), 10 deletions(-) diff --git a/registry/coder/modules/dotfiles/README.md b/registry/coder/modules/dotfiles/README.md index c78b80c3..aae52284 100644 --- a/registry/coder/modules/dotfiles/README.md +++ b/registry/coder/modules/dotfiles/README.md @@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/ module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.3.2" + version = "1.4.0" agent_id = coder_agent.example.id } ``` @@ -31,7 +31,7 @@ module "dotfiles" { module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.3.2" + version = "1.4.0" agent_id = coder_agent.example.id } ``` @@ -42,7 +42,7 @@ module "dotfiles" { module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.3.2" + version = "1.4.0" agent_id = coder_agent.example.id user = "root" } @@ -54,14 +54,14 @@ module "dotfiles" { module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.3.2" + version = "1.4.0" agent_id = coder_agent.example.id } module "dotfiles-root" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.3.2" + version = "1.4.0" agent_id = coder_agent.example.id user = "root" dotfiles_uri = module.dotfiles.dotfiles_uri @@ -90,7 +90,7 @@ You can set a default dotfiles repository for all users by setting the `default_ module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.3.2" + version = "1.4.0" agent_id = coder_agent.example.id default_dotfiles_uri = "https://github.com/coder/dotfiles" } diff --git a/registry/coder/modules/dotfiles/main.test.ts b/registry/coder/modules/dotfiles/main.test.ts index 8cde2510..a9a8bf93 100644 --- a/registry/coder/modules/dotfiles/main.test.ts +++ b/registry/coder/modules/dotfiles/main.test.ts @@ -62,7 +62,41 @@ describe("dotfiles", async () => { agent_id: "foo", coder_parameter_order: order.toString(), }); + expect(state.resources).toHaveLength(3); + const parameters = state.resources.filter( + (r) => r.type === "coder_parameter", + ); + for (const param of parameters) { + expect(param.instances[0].attributes.order).toBe(order); + } + }); + + it("set custom dotfiles_branch", async () => { + const branch = "develop"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + dotfiles_branch: branch, + }); expect(state.resources).toHaveLength(2); - expect(state.resources[0].instances[0].attributes.order).toBe(order); + const scriptResource = state.resources.find( + (r) => r.type === "coder_script", + ); + expect(scriptResource?.instances[0].attributes.script).toContain( + `DOTFILES_BRANCH="${branch}"`, + ); + }); + + it("default dotfiles_branch creates parameter", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + expect(state.resources).toHaveLength(3); + const branchParameter = state.resources.find( + (r) => + r.type === "coder_parameter" && + r.instances[0].attributes.name === "dotfiles_branch", + ); + expect(branchParameter).toBeDefined(); + expect(branchParameter?.instances[0].attributes.default).toBeNull(); }); }); diff --git a/registry/coder/modules/dotfiles/main.tf b/registry/coder/modules/dotfiles/main.tf index 7b15a391..ca1709d0 100644 --- a/registry/coder/modules/dotfiles/main.tf +++ b/registry/coder/modules/dotfiles/main.tf @@ -46,6 +46,12 @@ variable "default_dotfiles_uri" { } } +variable "default_dotfiles_branch" { + type = string + description = "The default dotfiles branch if the workspace user does not provide one" + default = "" +} + variable "dotfiles_uri" { type = string description = "The URL to a dotfiles repository. (optional, when set, the user isn't prompted for their dotfiles)" @@ -61,6 +67,17 @@ variable "dotfiles_uri" { } } +variable "dotfiles_branch" { + type = string + description = "The branch to use for the dotfiles repository (optional, when set, the user isn't prompted for the branch)" + default = null + + validation { + condition = var.dotfiles_branch == null || var.dotfiles_branch != "" + error_message = "dotfiles_branch cannot be an empty string. Use null to prompt the user or provide a valid branch name." + } +} + variable "user" { type = string description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)" @@ -107,8 +124,21 @@ data "coder_parameter" "dotfiles_uri" { } } +data "coder_parameter" "dotfiles_branch" { + count = var.dotfiles_branch == null ? 1 : 0 + type = "string" + name = "dotfiles_branch" + display_name = "Dotfiles Branch" + order = var.coder_parameter_order + default = var.default_dotfiles_branch + description = "The branch to use for the dotfiles repository" + mutable = true + icon = "/icon/dotfiles.svg" +} + locals { dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value + dotfiles_branch = var.dotfiles_branch != null ? var.dotfiles_branch : data.coder_parameter.dotfiles_branch[0].value user = var.user != null ? var.user : "" encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : "" } @@ -118,6 +148,7 @@ resource "coder_script" "dotfiles" { script = templatefile("${path.module}/run.sh", { DOTFILES_URI : local.dotfiles_uri, DOTFILES_USER : local.user, + DOTFILES_BRANCH : local.dotfiles_branch, POST_CLONE_SCRIPT : local.encoded_post_clone_script }) display_name = "Dotfiles" @@ -136,6 +167,7 @@ resource "coder_app" "dotfiles" { command = templatefile("${path.module}/run.sh", { DOTFILES_URI : local.dotfiles_uri, DOTFILES_USER : local.user, + DOTFILES_BRANCH : local.dotfiles_branch, POST_CLONE_SCRIPT : local.encoded_post_clone_script }) } diff --git a/registry/coder/modules/dotfiles/run.sh b/registry/coder/modules/dotfiles/run.sh index 49ab3ec5..f7f275f8 100644 --- a/registry/coder/modules/dotfiles/run.sh +++ b/registry/coder/modules/dotfiles/run.sh @@ -4,6 +4,7 @@ set -euo pipefail DOTFILES_URI="${DOTFILES_URI}" DOTFILES_USER="${DOTFILES_USER}" +DOTFILES_BRANCH="${DOTFILES_BRANCH}" # Validate DOTFILES_URI to prevent command injection (defense in depth) if [ -n "$DOTFILES_URI" ]; then @@ -24,10 +25,18 @@ if [ -n "$${DOTFILES_URI// }" ]; then DOTFILES_USER="$USER" fi - echo "✨ Applying dotfiles for user $DOTFILES_USER" + if [ -n "$DOTFILES_BRANCH" ]; then + echo "✨ Applying dotfiles for user $DOTFILES_USER from branch $DOTFILES_BRANCH" + else + echo "✨ Applying dotfiles for user $DOTFILES_USER" + fi if [ "$DOTFILES_USER" = "$USER" ]; then - coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log + if [ -n "$DOTFILES_BRANCH" ]; then + coder dotfiles "$DOTFILES_URI" --branch "$DOTFILES_BRANCH" -y 2>&1 | tee ~/.dotfiles.log + else + coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log + fi else if command -v getent > /dev/null 2>&1; then DOTFILES_USER_HOME=$(getent passwd "$DOTFILES_USER" | cut -d: -f6) @@ -40,7 +49,11 @@ if [ -n "$${DOTFILES_URI// }" ]; then fi CODER_BIN=$(command -v coder) - sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log" + if [ -n "$DOTFILES_BRANCH" ]; then + sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" --branch "$DOTFILES_BRANCH" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log" + else + sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log" + fi fi fi