From c819ca7f83d66a9f20ab58f136f50358f2aa0033 Mon Sep 17 00:00:00 2001
From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com>
Date: Mon, 5 Jan 2026 13:47:22 +0530
Subject: [PATCH 1/8] fix(nboyers/templates/cloud-dev): fix broken template
icon (#633)
## Description
## Type of Change
- [ ] New module
- [ ] New template
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other
## Template Information
**Path:** `registry/nboyers/templates/cloud-devops`
## Testing & Validation
- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally
## Related Issues
---
.icons/cloud-devops.svg | 29 +++++++++++++++++++
.../nboyers/templates/cloud-dev/README.md | 17 ++++-------
2 files changed, 34 insertions(+), 12 deletions(-)
create mode 100644 .icons/cloud-devops.svg
diff --git a/.icons/cloud-devops.svg b/.icons/cloud-devops.svg
new file mode 100644
index 00000000..a3b6e82a
--- /dev/null
+++ b/.icons/cloud-devops.svg
@@ -0,0 +1,29 @@
+
+
diff --git a/registry/nboyers/templates/cloud-dev/README.md b/registry/nboyers/templates/cloud-dev/README.md
index 257faf16..2131fbd0 100644
--- a/registry/nboyers/templates/cloud-dev/README.md
+++ b/registry/nboyers/templates/cloud-dev/README.md
@@ -1,16 +1,9 @@
---
-display_name: "Cloud DevOps Workspace"
-description: "A multi-cloud DevOps workspace that runs on Amazon EKS and provides authenticated access to AWS, Azure, and GCP."
-icon: "https://raw.githubusercontent.com/coder/coder-icons/main/icons/cloud-devops.svg"
-tags:
- - devops
- - kubernetes
- - aws
- - eks
- - multi-cloud
- - terraform
- - cdk
- - pulumi
+display_name: Cloud DevOps Workspace
+description: A multi-cloud DevOps workspace that runs on Amazon EKS and provides authenticated access to AWS, Azure, and GCP.
+icon: ../../../../.icons/cloud-devops.svg
+verified: false
+tags: [devops, kubernetes, aws, eks, multi-cloud, terraform, cdk, pulumi]
---
# Cloud DevOps Workspace
From 99bd4a4139e8d4d49e3fa225538d8475529b5ec6 Mon Sep 17 00:00:00 2001
From: DevCats
Date: Mon, 5 Jan 2026 14:59:06 -0600
Subject: [PATCH 2/8] fix(github-upload-public-key): resolve issues with flaky
tests (#634)
## Description
Better test cleanup, and resolve flakiness.
## Type of Change
- [ ] New module
- [ ] New template
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other
## Module Information
**Path:** `registry/coder/modules/github-upload-public-key`
**New version:** `v1.0.32`
**Breaking change:** [ ] Yes [ ] No
## Testing & Validation
- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [X] Changes tested locally
## Related Issues
---
.../github-upload-public-key/main.test.ts | 150 +++++++++++-------
1 file changed, 92 insertions(+), 58 deletions(-)
diff --git a/registry/coder/modules/github-upload-public-key/main.test.ts b/registry/coder/modules/github-upload-public-key/main.test.ts
index 467d6b95..6d8aae27 100644
--- a/registry/coder/modules/github-upload-public-key/main.test.ts
+++ b/registry/coder/modules/github-upload-public-key/main.test.ts
@@ -1,9 +1,17 @@
-import { type Server, serve } from "bun";
-import { describe, expect, it } from "bun:test";
+import { serve } from "bun";
+import {
+ afterEach,
+ beforeAll,
+ describe,
+ expect,
+ it,
+ setDefaultTimeout,
+} from "bun:test";
import {
createJSONResponse,
execContainer,
findResourceInstance,
+ removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
@@ -11,77 +19,48 @@ import {
writeCoder,
} from "~test";
-describe("github-upload-public-key", async () => {
- await runTerraformInit(import.meta.dir);
-
- testRequiredVariables(import.meta.dir, {
- agent_id: "foo",
- });
-
- it("creates new key if one does not exist", async () => {
- const { instance, id, server } = await setupContainer();
- await writeCoder(id, "echo foo");
-
- const url = server.url.toString().slice(0, -1);
- const exec = await execContainer(id, [
- "env",
- `CODER_ACCESS_URL=${url}`,
- `GITHUB_API_URL=${url}`,
- "CODER_OWNER_SESSION_TOKEN=foo",
- "CODER_EXTERNAL_AUTH_ID=github",
- "bash",
- "-c",
- instance.script,
- ]);
- expect(exec.stdout).toContain(
- "Your Coder public key has been added to GitHub!",
- );
- expect(exec.exitCode).toBe(0);
- // we need to increase timeout to pull the container
- }, 15000);
-
- it("does nothing if one already exists", async () => {
- const { instance, id, server } = await setupContainer();
- // use keyword to make server return a existing key
- await writeCoder(id, "echo findkey");
-
- const url = server.url.toString().slice(0, -1);
- const exec = await execContainer(id, [
- "env",
- `CODER_ACCESS_URL=${url}`,
- `GITHUB_API_URL=${url}`,
- "CODER_OWNER_SESSION_TOKEN=foo",
- "CODER_EXTERNAL_AUTH_ID=github",
- "bash",
- "-c",
- instance.script,
- ]);
- expect(exec.stdout).toContain(
- "Your Coder public key is already on GitHub!",
- );
- expect(exec.exitCode).toBe(0);
- });
+let cleanupFunctions: (() => Promise)[] = [];
+const registerCleanup = (cleanup: () => Promise) => {
+ cleanupFunctions.push(cleanup);
+};
+afterEach(async () => {
+ const cleanupFnsCopy = cleanupFunctions.slice().reverse();
+ cleanupFunctions = [];
+ for (const cleanup of cleanupFnsCopy) {
+ try {
+ await cleanup();
+ } catch (error) {
+ console.error("Error during cleanup:", error);
+ }
+ }
});
const setupContainer = async (
image = "lorello/alpine-bash",
vars: Record = {},
) => {
- const server = await setupServer();
+ const server = setupServer();
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
...vars,
});
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer(image);
+
+ registerCleanup(async () => {
+ server.stop();
+ });
+ registerCleanup(async () => {
+ await removeContainer(id);
+ });
+
return { id, instance, server };
};
-const setupServer = async (): Promise => {
- let url: URL;
- const fakeSlackHost = serve({
+const setupServer = () => {
+ const fakeGithubHost = serve({
fetch: (req) => {
- url = new URL(req.url);
+ const url = new URL(req.url);
if (url.pathname === "/api/v2/users/me/gitsshkey") {
return createJSONResponse({
public_key: "exists",
@@ -128,5 +107,60 @@ const setupServer = async (): Promise => {
port: 0,
});
- return fakeSlackHost;
+ return fakeGithubHost;
};
+
+setDefaultTimeout(30 * 1000);
+
+describe("github-upload-public-key", () => {
+ beforeAll(async () => {
+ await runTerraformInit(import.meta.dir);
+ });
+
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "foo",
+ });
+
+ it("creates new key if one does not exist", async () => {
+ const { instance, id, server } = await setupContainer();
+ await writeCoder(id, "echo foo");
+
+ const url = server.url.toString().slice(0, -1);
+ const exec = await execContainer(id, [
+ "env",
+ `CODER_ACCESS_URL=${url}`,
+ `GITHUB_API_URL=${url}`,
+ "CODER_OWNER_SESSION_TOKEN=foo",
+ "CODER_EXTERNAL_AUTH_ID=github",
+ "bash",
+ "-c",
+ instance.script,
+ ]);
+ expect(exec.stdout).toContain(
+ "Your Coder public key has been added to GitHub!",
+ );
+ expect(exec.exitCode).toBe(0);
+ });
+
+ it("does nothing if one already exists", async () => {
+ const { instance, id, server } = await setupContainer();
+ // use keyword to make server return a existing key
+ await writeCoder(id, "echo findkey");
+
+ const url = server.url.toString().slice(0, -1);
+ const exec = await execContainer(id, [
+ "env",
+ `CODER_ACCESS_URL=${url}`,
+ `GITHUB_API_URL=${url}`,
+ "CODER_OWNER_SESSION_TOKEN=foo",
+ "CODER_EXTERNAL_AUTH_ID=github",
+ "bash",
+ "-c",
+ instance.script,
+ ]);
+ expect(exec.stdout).toContain(
+ "Your Coder public key is already on GitHub!",
+ );
+ expect(exec.exitCode).toBe(0);
+ });
+});
From cbb39bda6f845ed3e21d08135296abfa8862a0f2 Mon Sep 17 00:00:00 2001
From: DevCats
Date: Mon, 5 Jan 2026 15:59:03 -0600
Subject: [PATCH 3/8] chore: remove test from cloud-dev template (#635)
## Description
Remove test from `cloud-dev` template since templates generally have no
tests.
## Type of Change
- [ ] New module
- [ ] New template
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other
## Template Information
**Path:** `registry/nboyers/templates/cloud-dev`
## Testing & Validation
- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [X] Changes tested locally
## Related Issues
---
.../templates/cloud-dev/test/basic.tftest.hcl | 87 -------------------
1 file changed, 87 deletions(-)
delete mode 100644 registry/nboyers/templates/cloud-dev/test/basic.tftest.hcl
diff --git a/registry/nboyers/templates/cloud-dev/test/basic.tftest.hcl b/registry/nboyers/templates/cloud-dev/test/basic.tftest.hcl
deleted file mode 100644
index 9009bbf4..00000000
--- a/registry/nboyers/templates/cloud-dev/test/basic.tftest.hcl
+++ /dev/null
@@ -1,87 +0,0 @@
-# Run 'terraform test' from this template directory (where main.tf lives)
-
-# --- Mock cloud providers so no external calls happen ---
-mock_provider "aws" {}
-mock_provider "kubernetes" {}
-
-# Provide fake values for data sources your template reads
-override_data {
- target = data.aws_eks_cluster.eks
- values = {
- name = "unit-test-eks"
- endpoint = "https://example.eks.local"
- certificate_authority = [{
- data = base64encode("dummy-ca")
- }]
- }
-}
-
-override_data {
- target = data.aws_eks_cluster_auth.eks
- values = {
- token = "dummy-token"
- }
-}
-
-# ---------------------------
-# 1) Validate configuration
-# ---------------------------
-run "validate" {
- command = validate
-}
-
-# ---------------------------
-# 2) Plan with representative inputs
-# ---------------------------
-run "plan_with_defaults" {
- command = plan
-
- variables {
- host_cluster_name = "unit-test-eks"
-
- # IaC/tooling toggles
- iac_tool = "terraform"
- enable_aws = true
- enable_azure = false
- enable_gcp = false
-
- # Dev creds (empty OK for unit test)
- aws_access_key_id = ""
- aws_secret_access_key = ""
- azure_client_id = ""
- azure_tenant_id = ""
- azure_client_secret = ""
- gcp_service_account = ""
- }
-
- # Simple sanity assertions (adjust resource addresses to your template)
- assert {
- condition = can(resource.kubernetes_namespace.workspace)
- error_message = "kubernetes_namespace.workspace was not created in plan."
- }
-
- assert {
- condition = can(resource.coder_agent.main)
- error_message = "coder_agent.main was not planned."
- }
-}
-
-# ---------------------------
-# 3) Plan with CDK selected
-# ---------------------------
-run "plan_with_cdk" {
- command = plan
- variables {
- host_cluster_name = "unit-test-eks"
- iac_tool = "cdk"
- enable_aws = true
- enable_azure = false
- enable_gcp = false
- }
-
- # Ensure the env reflects choice (string map lookup)
- assert {
- condition = contains(keys(resource.coder_agent.main.env), "IAC_TOOL")
- error_message = "IAC_TOOL env not present on coder_agent.main."
- }
-}
From 7df0cb25c5fcac2193967423796dba321c63a62b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Jan 2026 22:10:18 +0000
Subject: [PATCH 4/8] chore(deps): bump crate-ci/typos from 1.40.0 to 1.41.0 in
the github-actions group (#632)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/ci.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 5601ef6a..bc16a5b8 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -93,7 +93,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
- uses: crate-ci/typos@v1.40.0
+ uses: crate-ci/typos@v1.41.0
with:
config: .github/typos.toml
validate-readme-files:
From 60611ed593f59716b7244f027c57a3c98d559959 Mon Sep 17 00:00:00 2001
From: Scai <59282365+alexevladgabriel@users.noreply.github.com>
Date: Tue, 6 Jan 2026 16:29:40 +0200
Subject: [PATCH 5/8] feat: make dynamic locations & server types on Hetzner
template (#618)
## Description
Make Server Types & Locations dynamic based on API endpoints provided by
Hetzner Docs.
## Type of Change
- [ ] New module
- [ ] New template
- [ ] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other
## Template Information
**Path:** `registry/Excellencedev/templates/hetzner-linux`
## Testing & Validation
- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: DevCats
---
.../hetzner-linux/hetzner_server_types.json | 27 -----
.../templates/hetzner-linux/main.tf | 105 ++++++++++++------
2 files changed, 73 insertions(+), 59 deletions(-)
delete mode 100644 registry/Excellencedev/templates/hetzner-linux/hetzner_server_types.json
diff --git a/registry/Excellencedev/templates/hetzner-linux/hetzner_server_types.json b/registry/Excellencedev/templates/hetzner-linux/hetzner_server_types.json
deleted file mode 100644
index 6be0938a..00000000
--- a/registry/Excellencedev/templates/hetzner-linux/hetzner_server_types.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "type_meta": {
- "cx22": { "cores": 2, "memory_gb": 4, "disk_gb": 40 },
- "cx32": { "cores": 4, "memory_gb": 8, "disk_gb": 80 },
- "cx42": { "cores": 8, "memory_gb": 16, "disk_gb": 160 },
- "cx52": { "cores": 16, "memory_gb": 32, "disk_gb": 320 },
- "cpx11": { "cores": 2, "memory_gb": 2, "disk_gb": 40 },
- "cpx21": { "cores": 3, "memory_gb": 4, "disk_gb": 80 },
- "cpx31": { "cores": 4, "memory_gb": 8, "disk_gb": 160 },
- "cpx41": { "cores": 8, "memory_gb": 16, "disk_gb": 240 },
- "cpx51": { "cores": 16, "memory_gb": 32, "disk_gb": 360 },
- "ccx13": { "cores": 2, "memory_gb": 8, "disk_gb": 80 },
- "ccx23": { "cores": 4, "memory_gb": 16, "disk_gb": 160 },
- "ccx33": { "cores": 8, "memory_gb": 32, "disk_gb": 240 },
- "ccx43": { "cores": 16, "memory_gb": 64, "disk_gb": 360 },
- "ccx53": { "cores": 32, "memory_gb": 128, "disk_gb": 600 },
- "ccx63": { "cores": 48, "memory_gb": 192, "disk_gb": 960 }
- },
- "availability": {
- "fsn1": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
- "ash": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
- "hel1": ["cx22", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
- "hil": ["cpx11", "cpx21", "cpx31", "cpx41", "ccx13", "ccx23", "ccx33"],
- "nbg1": ["cx22", "cx32", "cx42", "cx52", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
- "sin": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"]
- }
-}
diff --git a/registry/Excellencedev/templates/hetzner-linux/main.tf b/registry/Excellencedev/templates/hetzner-linux/main.tf
index 4b71e456..03e01a10 100644
--- a/registry/Excellencedev/templates/hetzner-linux/main.tf
+++ b/registry/Excellencedev/templates/hetzner-linux/main.tf
@@ -6,6 +6,10 @@ terraform {
coder = {
source = "coder/coder"
}
+ http = {
+ source = "hashicorp/http"
+ version = "~> 3.0"
+ }
}
}
@@ -17,6 +21,24 @@ provider "hcloud" {
token = var.hcloud_token
}
+data "http" "hcloud_locations" {
+ url = "https://api.hetzner.cloud/v1/locations"
+
+ request_headers = {
+ Authorization = "Bearer ${var.hcloud_token}"
+ Accept = "application/json"
+ }
+}
+
+data "http" "hcloud_server_types" {
+ url = "https://api.hetzner.cloud/v1/server_types"
+
+ request_headers = {
+ Authorization = "Bearer ${var.hcloud_token}"
+ Accept = "application/json"
+ }
+}
+
# Available locations: https://docs.hetzner.com/cloud/general/locations/
data "coder_parameter" "hcloud_location" {
name = "hcloud_location"
@@ -24,29 +46,18 @@ data "coder_parameter" "hcloud_location" {
description = "Select the Hetzner Cloud location for your workspace."
type = "string"
default = "fsn1"
- option {
- name = "DE Falkenstein"
- value = "fsn1"
- }
- option {
- name = "US Ashburn, VA"
- value = "ash"
- }
- option {
- name = "US Hillsboro, OR"
- value = "hil"
- }
- option {
- name = "SG Singapore"
- value = "sin"
- }
- option {
- name = "DE Nuremberg"
- value = "nbg1"
- }
- option {
- name = "FI Helsinki"
- value = "hel1"
+
+ dynamic "option" {
+ for_each = local.hcloud_locations
+ content {
+ name = format(
+ "%s (%s, %s)",
+ upper(option.value.name),
+ option.value.city,
+ option.value.country
+ )
+ value = option.value.name
+ }
}
}
@@ -109,17 +120,47 @@ resource "hcloud_volume_attachment" "home_volume_attachment" {
locals {
username = lower(data.coder_workspace_owner.me.name)
- # Data source: local JSON file under the module directory
- # Check API for latest server types & availability: https://docs.hetzner.cloud/reference/cloud#server-types
- hcloud_server_types_data = jsondecode(file("${path.module}/hetzner_server_types.json"))
- hcloud_server_type_meta = local.hcloud_server_types_data.type_meta
- hcloud_server_types_by_location = local.hcloud_server_types_data.availability
+ # --------------------
+ # Locations
+ # --------------------
+ hcloud_locations = [
+ for loc in jsondecode(data.http.hcloud_locations.response_body).locations : {
+ name = loc.name
+ city = loc.city
+ country = loc.country
+ }
+ ]
+
+ # --------------------
+ # Server Types
+ # --------------------
+ hcloud_server_types = {
+ for st in jsondecode(data.http.hcloud_server_types.response_body).server_types :
+ st.name => {
+ cores = st.cores
+ memory_gb = st.memory
+ disk_gb = st.disk
+ locations = [for l in st.locations : l.name]
+ deprecated = st.deprecated
+ }
+ if st.deprecated == false
+ }
hcloud_server_type_options_for_selected_location = [
- for type_name in lookup(local.hcloud_server_types_by_location, data.coder_parameter.hcloud_location.value, []) : {
- name = format("%s (%d vCPU, %dGB RAM, %dGB)", upper(type_name), local.hcloud_server_type_meta[type_name].cores, local.hcloud_server_type_meta[type_name].memory_gb, local.hcloud_server_type_meta[type_name].disk_gb)
- value = type_name
+ for name, meta in local.hcloud_server_types : {
+ name = format(
+ "%s (%d vCPU, %dGB RAM, %dGB)",
+ upper(name),
+ meta.cores,
+ meta.memory_gb,
+ meta.disk_gb
+ )
+ value = name
}
+ if contains(
+ meta.locations,
+ data.coder_parameter.hcloud_location.value
+ )
]
}
@@ -180,4 +221,4 @@ module "code-server" {
agent_id = coder_agent.main.id
order = 1
-}
\ No newline at end of file
+}
From 2701dc09afb4a3ac7258d95a2a6c2106de54ad16 Mon Sep 17 00:00:00 2001
From: Atif Ali
Date: Wed, 7 Jan 2026 01:06:31 +0500
Subject: [PATCH 6/8] feat(coder/modules/jetbrains): update to latest build
numbers and clean up tests (#636)
Co-authored-by: DevCats
---
registry/coder/modules/jetbrains/README.md | 24 +-
.../modules/jetbrains/jetbrains.tftest.hcl | 85 +-
registry/coder/modules/jetbrains/main.test.ts | 1054 -----------------
registry/coder/modules/jetbrains/main.tf | 55 +-
4 files changed, 112 insertions(+), 1106 deletions(-)
delete mode 100644 registry/coder/modules/jetbrains/main.test.ts
diff --git a/registry/coder/modules/jetbrains/README.md b/registry/coder/modules/jetbrains/README.md
index 71861359..cf97d127 100644
--- a/registry/coder/modules/jetbrains/README.md
+++ b/registry/coder/modules/jetbrains/README.md
@@ -14,10 +14,9 @@ This module adds JetBrains IDE buttons to launch IDEs directly from the dashboar
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.2.1"
+ version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
- # tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button." # Optional
}
```
@@ -40,7 +39,7 @@ When `default` contains IDE codes, those IDEs are created directly without user
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.2.1"
+ version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
@@ -53,7 +52,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.2.1"
+ version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
# Show parameter with limited options
@@ -67,7 +66,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.2.1"
+ version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
default = ["IU", "PY"]
@@ -82,7 +81,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.2.1"
+ version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/workspace/project"
@@ -109,7 +108,7 @@ module "jetbrains" {
module "jetbrains_pycharm" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.2.1"
+ version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/workspace/project"
@@ -129,11 +128,11 @@ Add helpful tooltip text that appears when users hover over the IDE app buttons:
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.2.1"
+ version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
default = ["IU", "PY"]
- tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
+ tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
}
```
@@ -170,13 +169,6 @@ resource "coder_metadata" "container_info" {
- If the API is unreachable (air-gapped environments), the module automatically falls back to build numbers from `ide_config`
- `major_version` and `channel` control which API endpoint is queried (when API access is available)
-### Tooltip
-
-- **`tooltip`**: Optional markdown text displayed when hovering over IDE app buttons
-- If not specified, no tooltip is shown
-- Supports markdown formatting for rich text (bold, italic, links, etc.)
-- All IDE apps created by this module will show the same tooltip text
-
## Supported IDEs
All JetBrains IDEs with remote development capabilities:
diff --git a/registry/coder/modules/jetbrains/jetbrains.tftest.hcl b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl
index 21726c25..dba9551d 100644
--- a/registry/coder/modules/jetbrains/jetbrains.tftest.hcl
+++ b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl
@@ -2,15 +2,15 @@ variables {
# Default IDE config, mirrored from main.tf for test assertions.
# If main.tf defaults change, update this map to match.
expected_ide_config = {
- "CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
- "GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
- "IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
- "PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
- "PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
- "RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
- "RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
- "RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
- "WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
+ "CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
+ "GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
+ "IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
+ "PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" },
+ "PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" },
+ "RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" },
+ "RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" },
+ "RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" },
+ "WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" }
}
}
@@ -187,16 +187,16 @@ run "tooltip_when_provided" {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
- tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
+ tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
}
assert {
- condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."])
+ condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."])
error_message = "Expected coder_app tooltip to be set when provided"
}
}
-run "tooltip_null_when_not_provided" {
+run "tooltip_default_when_not_provided" {
command = plan
variables {
@@ -206,8 +206,41 @@ run "tooltip_null_when_not_provided" {
}
assert {
- condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == null])
- error_message = "Expected coder_app tooltip to be null when not provided"
+ condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."])
+ error_message = "Expected coder_app tooltip to be the default JetBrains Toolbox message when not provided"
+ }
+}
+
+run "channel_eap" {
+ command = plan
+
+ variables {
+ agent_id = "foo"
+ folder = "/home/coder"
+ default = ["GO"]
+ channel = "eap"
+ major_version = "latest"
+ }
+
+ assert {
+ condition = output.ide_metadata["GO"].json_data.type == "eap"
+ error_message = "Expected the API to return a release of type 'eap', but got '${output.ide_metadata["GO"].json_data.type}'"
+ }
+}
+
+run "specific_major_version" {
+ command = plan
+
+ variables {
+ agent_id = "foo"
+ folder = "/home/coder"
+ default = ["GO"]
+ major_version = "2025.3"
+ }
+
+ assert {
+ condition = output.ide_metadata["GO"].json_data.majorVersion == "2025.3"
+ error_message = "Expected the API to return a release for major version '2025.3', but got '${output.ide_metadata["GO"].json_data.majorVersion}'"
}
}
@@ -294,3 +327,27 @@ run "output_multiple_ides" {
error_message = "Expected ide_metadata['PY'].build to be the fallback '${var.expected_ide_config["PY"].build}'"
}
}
+run "validate_output_schema" {
+ command = plan
+
+ variables {
+ agent_id = "foo"
+ folder = "/home/coder"
+ default = ["GO"]
+ }
+
+ assert {
+ condition = alltrue([
+ for key, meta in output.ide_metadata : (
+ can(meta.icon) &&
+ can(meta.name) &&
+ can(meta.identifier) &&
+ can(meta.key) &&
+ can(meta.build) &&
+ # json_data can be null, but the key must exist
+ can(meta.json_data)
+ )
+ ])
+ error_message = "The ide_metadata output schema has changed. Please update the 'main.tf' and this test."
+ }
+}
diff --git a/registry/coder/modules/jetbrains/main.test.ts b/registry/coder/modules/jetbrains/main.test.ts
deleted file mode 100644
index 0acf2ec2..00000000
--- a/registry/coder/modules/jetbrains/main.test.ts
+++ /dev/null
@@ -1,1054 +0,0 @@
-import { it, expect, describe } from "bun:test";
-import {
- runTerraformInit,
- testRequiredVariables,
- runTerraformApply,
-} from "~test";
-
-describe("jetbrains", async () => {
- await runTerraformInit(import.meta.dir);
-
- await testRequiredVariables(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/foo",
- });
-
- // Core Logic Tests - When default is empty (shows parameter)
- describe("when default is empty (shows parameter)", () => {
- it("should create parameter with all IDE options when default=[] and major_version=latest", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- major_version: "latest",
- });
-
- // Should create a parameter when default is empty
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter).toBeDefined();
- expect(parameter?.instances[0].attributes.form_type).toBe("multi-select");
- expect(parameter?.instances[0].attributes.default).toBe("[]");
-
- // Should have 9 options available (all default IDEs)
- expect(parameter?.instances[0].attributes.option).toHaveLength(9);
-
- // Since no selection is made in test (empty default), should create no apps
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(0);
- });
-
- it("should create parameter with all IDE options when default=[] and major_version=2025.1", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- major_version: "2025.1",
- });
-
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter).toBeDefined();
- expect(parameter?.instances[0].attributes.option).toHaveLength(9);
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(0);
- });
-
- it("should create parameter with custom options when default=[] and custom options", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- options: '["GO", "IU", "WS"]',
- major_version: "latest",
- });
-
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter).toBeDefined();
- expect(parameter?.instances[0].attributes.option).toHaveLength(3); // Only custom options
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(0);
- });
-
- it("should create parameter with single option when default=[] and single option", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- options: '["GO"]',
- major_version: "latest",
- });
-
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter).toBeDefined();
- expect(parameter?.instances[0].attributes.option).toHaveLength(1);
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(0);
- });
- });
-
- // Core Logic Tests - When default has values (skips parameter, creates apps directly)
- describe("when default has values (creates apps directly)", () => {
- it('should skip parameter and create single app when default=["GO"] and major_version=latest', async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- major_version: "latest",
- });
-
- // Should NOT create a parameter when default is not empty
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter).toBeUndefined();
-
- // Should create exactly 1 app
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(1);
- expect(coder_apps[0].instances[0].attributes.slug).toBe("jetbrains-go");
- expect(coder_apps[0].instances[0].attributes.display_name).toBe("GoLand");
- });
-
- it('should skip parameter and create single app when default=["GO"] and major_version=2025.1', async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- major_version: "2025.1",
- });
-
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter).toBeUndefined();
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(1);
- expect(coder_apps[0].instances[0].attributes.display_name).toBe("GoLand");
- });
-
- it("should skip parameter and create app with different IDE", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["RR"]',
- major_version: "latest",
- });
-
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter).toBeUndefined();
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(1);
- expect(coder_apps[0].instances[0].attributes.slug).toBe("jetbrains-rr");
- expect(coder_apps[0].instances[0].attributes.display_name).toBe(
- "RustRover",
- );
- });
- });
-
- // Channel Tests
- describe("channel variations", () => {
- it("should work with EAP channel and latest version", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- major_version: "latest",
- channel: "eap",
- });
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(1);
-
- // Check that URLs contain build numbers (from EAP releases)
- expect(coder_apps[0].instances[0].attributes.url).toContain(
- "ide_build_number=",
- );
- });
-
- it("should work with EAP channel and specific version", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- major_version: "2025.2",
- channel: "eap",
- });
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(1);
- expect(coder_apps[0].instances[0].attributes.url).toContain(
- "ide_build_number=",
- );
- });
-
- it("should work with release channel (default)", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- channel: "release",
- });
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(1);
- });
- });
-
- // Configuration Tests
- describe("configuration parameters", () => {
- it("should use custom folder path in URL", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/workspace/myproject",
- default: '["GO"]',
- major_version: "latest",
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_app?.instances[0].attributes.url).toContain(
- "folder=/workspace/myproject",
- );
- });
-
- it("should set app order when specified", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- coder_app_order: 10,
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_app?.instances[0].attributes.order).toBe(10);
- });
-
- it("should set parameter order when default is empty", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- coder_parameter_order: 5,
- });
-
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter?.instances[0].attributes.order).toBe(5);
- });
-
- it("should set tooltip when specified", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- tooltip:
- "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button.",
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_app?.instances[0].attributes.tooltip).toBe(
- "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button.",
- );
- });
-
- it("should have null tooltip when not specified", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_app?.instances[0].attributes.tooltip).toBeNull();
- });
- });
-
- // URL Generation Tests
- describe("URL generation", () => {
- it("should generate proper jetbrains:// URLs with all required parameters", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "test-agent-123",
- folder: "/custom/project/path",
- default: '["GO"]',
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- const url = coder_app?.instances[0].attributes.url;
-
- expect(url).toContain("jetbrains://gateway/coder");
- expect(url).toContain("&workspace=");
- expect(url).toContain("&owner=");
- expect(url).toContain("&folder=/custom/project/path");
- expect(url).toContain("&url=");
- expect(url).toContain("&token=$SESSION_TOKEN");
- expect(url).toContain("&ide_product_code=GO");
- expect(url).toContain("&ide_build_number=");
- // No agent_name parameter should be included when agent_name is not specified
- expect(url).not.toContain("&agent_name=");
- });
-
- it("should include agent_name parameter when agent_name is specified", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "test-agent-123",
- agent_name: "main-agent",
- folder: "/custom/project/path",
- default: '["GO"]',
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- const url = coder_app?.instances[0].attributes.url;
-
- expect(url).toContain("jetbrains://gateway/coder");
- expect(url).toContain("&agent_name=main-agent");
- expect(url).toContain("&ide_product_code=GO");
- expect(url).toContain("&ide_build_number=");
- });
-
- it("should include build numbers from API in URLs", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- const url = coder_app?.instances[0].attributes.url;
-
- expect(url).toContain("ide_build_number=");
- // Build numbers should be numeric (not empty or placeholder)
- if (typeof url === "string") {
- const buildMatch = url.match(/ide_build_number=([^&]+)/);
- expect(buildMatch).toBeTruthy();
- expect(buildMatch![1]).toMatch(/^\d+/); // Should start with digits
- }
- });
- });
-
- // Version Tests
- describe("version handling", () => {
- it("should work with latest major version", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- major_version: "latest",
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_app?.instances[0].attributes.url).toContain(
- "ide_build_number=",
- );
- });
-
- it("should work with specific major version", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- major_version: "2025.1",
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_app?.instances[0].attributes.url).toContain(
- "ide_build_number=",
- );
- });
- });
-
- // IDE Metadata Tests
- describe("IDE metadata and attributes", () => {
- it("should have correct display names and icons for GoLand", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
-
- expect(coder_app?.instances[0].attributes.display_name).toBe("GoLand");
- expect(coder_app?.instances[0].attributes.icon).toBe("/icon/goland.svg");
- expect(coder_app?.instances[0].attributes.slug).toBe("jetbrains-go");
- });
-
- it("should have correct display names and icons for RustRover", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["RR"]',
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
-
- expect(coder_app?.instances[0].attributes.display_name).toBe("RustRover");
- expect(coder_app?.instances[0].attributes.icon).toBe(
- "/icon/rustrover.svg",
- );
- expect(coder_app?.instances[0].attributes.slug).toBe("jetbrains-rr");
- });
-
- it("should have correct app attributes set", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "test-agent",
- folder: "/home/coder",
- default: '["GO"]',
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
-
- expect(coder_app?.instances[0].attributes.agent_id).toBe("test-agent");
- expect(coder_app?.instances[0].attributes.external).toBe(true);
- expect(coder_app?.instances[0].attributes.hidden).toBe(false);
- expect(coder_app?.instances[0].attributes.share).toBe("owner");
- expect(coder_app?.instances[0].attributes.open_in).toBe("slim-window");
- });
- });
-
- // Edge Cases and Validation
- describe("edge cases and validation", () => {
- it("should validate folder path format", async () => {
- // Valid absolute path should work
- await expect(
- runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder/project",
- default: '["GO"]',
- }),
- ).resolves.toBeDefined();
- });
-
- it("should handle empty parameter selection gracefully", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- // Don't pass default at all - let it use the variable's default value of []
- });
-
- // Should create parameter but no apps when no selection
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter).toBeDefined();
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(0);
- });
- });
-
- // Custom IDE Config Tests
- describe("custom ide_config with subset of options", () => {
- const customIdeConfig = JSON.stringify({
- GO: {
- name: "Custom GoLand",
- icon: "/custom/goland.svg",
- build: "999.123.456",
- },
- IU: {
- name: "Custom IntelliJ",
- icon: "/custom/intellij.svg",
- build: "999.123.457",
- },
- WS: {
- name: "Custom WebStorm",
- icon: "/custom/webstorm.svg",
- build: "999.123.458",
- },
- });
-
- it("should handle multiple defaults without custom ide_config (debug test)", async () => {
- const testParams = {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO", "IU"]', // Test multiple defaults without custom config
- };
-
- const state = await runTerraformApply(import.meta.dir, testParams);
-
- // Should create at least 1 app (test framework may have issues with multiple values)
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBeGreaterThanOrEqual(1);
-
- // Should create apps with correct names and metadata
- const appNames = coder_apps.map(
- (app) => app.instances[0].attributes.display_name,
- );
- expect(appNames).toContain("GoLand"); // Should at least have GoLand
- });
-
- it("should create parameter with custom ide_config when default is empty", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- // Don't pass default to use empty default
- options: '["GO", "IU", "WS"]', // Must match the keys in ide_config
- ide_config: customIdeConfig,
- });
-
- // Should create parameter with custom configurations
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter).toBeDefined();
- expect(parameter?.instances[0].attributes.option).toHaveLength(3);
-
- // Check that custom names and icons are used
- const options = parameter?.instances[0].attributes.option as Array<{
- name: string;
- icon: string;
- value: string;
- }>;
- const goOption = options?.find((opt) => opt.value === "GO");
- expect(goOption?.name).toBe("Custom GoLand");
- expect(goOption?.icon).toBe("/custom/goland.svg");
-
- const iuOption = options?.find((opt) => opt.value === "IU");
- expect(iuOption?.name).toBe("Custom IntelliJ");
- expect(iuOption?.icon).toBe("/custom/intellij.svg");
-
- // Should create no apps since no selection
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(0);
- });
-
- it("should create apps with custom ide_config when default has values", async () => {
- const testParams = {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO", "IU"]', // Subset of available options
- options: '["GO", "IU", "WS"]', // Must be superset of default
- ide_config: customIdeConfig,
- };
-
- const state = await runTerraformApply(import.meta.dir, testParams);
-
- // Should NOT create parameter when default is not empty
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter).toBeUndefined();
-
- // Should create at least 1 app with custom configurations (test framework may have issues with multiple values)
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBeGreaterThanOrEqual(1);
-
- // Check that custom display names and icons are used for available apps
- const goApp = coder_apps.find(
- (app) => app.instances[0].attributes.slug === "jetbrains-go",
- );
- if (goApp) {
- expect(goApp.instances[0].attributes.display_name).toBe(
- "Custom GoLand",
- );
- expect(goApp.instances[0].attributes.icon).toBe("/custom/goland.svg");
- }
-
- const iuApp = coder_apps.find(
- (app) => app.instances[0].attributes.slug === "jetbrains-iu",
- );
- if (iuApp) {
- expect(iuApp.instances[0].attributes.display_name).toBe(
- "Custom IntelliJ",
- );
- expect(iuApp.instances[0].attributes.icon).toBe("/custom/intellij.svg");
- }
-
- // At least one app should be created
- expect(coder_apps.length).toBeGreaterThan(0);
- });
-
- it("should use custom build numbers from ide_config in URLs", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- options: '["GO", "IU", "WS"]',
- ide_config: customIdeConfig,
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
-
- // Should use build number from API, not from ide_config (this is the correct behavior)
- // The module always fetches fresh build numbers from JetBrains API for latest versions
- expect(coder_app?.instances[0].attributes.url).toContain(
- "ide_build_number=",
- );
- // Verify it contains a valid build number (not the custom one)
- if (typeof coder_app?.instances[0].attributes.url === "string") {
- const buildMatch = coder_app.instances[0].attributes.url.match(
- /ide_build_number=([^&]+)/,
- );
- expect(buildMatch).toBeTruthy();
- expect(buildMatch![1]).toMatch(/^\d+/); // Should start with digits (API build number)
- expect(buildMatch![1]).not.toBe("999.123.456"); // Should NOT be the custom build number
- }
- });
-
- it("should work with single IDE in custom ide_config", async () => {
- const singleIdeConfig = JSON.stringify({
- RR: {
- name: "My RustRover",
- icon: "/my/rustrover.svg",
- build: "888.999.111",
- },
- });
-
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["RR"]',
- options: '["RR"]', // Only one option
- ide_config: singleIdeConfig,
- });
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
- expect(coder_apps.length).toBe(1);
- expect(coder_apps[0].instances[0].attributes.display_name).toBe(
- "My RustRover",
- );
- expect(coder_apps[0].instances[0].attributes.icon).toBe(
- "/my/rustrover.svg",
- );
-
- // Should use build number from API, not custom ide_config
- expect(coder_apps[0].instances[0].attributes.url).toContain(
- "ide_build_number=",
- );
- if (typeof coder_apps[0].instances[0].attributes.url === "string") {
- const buildMatch = coder_apps[0].instances[0].attributes.url.match(
- /ide_build_number=([^&]+)/,
- );
- expect(buildMatch).toBeTruthy();
- expect(buildMatch![1]).not.toBe("888.999.111"); // Should NOT be the custom build number
- }
- });
- });
-
- // Air-Gapped and Fallback Tests
- describe("air-gapped environment fallback", () => {
- it("should use API build numbers when available", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
-
- // Should use build number from API
- expect(coder_app?.instances[0].attributes.url).toContain(
- "ide_build_number=",
- );
- if (typeof coder_app?.instances[0].attributes.url === "string") {
- const buildMatch = coder_app.instances[0].attributes.url.match(
- /ide_build_number=([^&]+)/,
- );
- expect(buildMatch).toBeTruthy();
- expect(buildMatch![1]).toMatch(/^\d+/); // Should be a valid build number from API
- // Should NOT be the default fallback build number
- expect(buildMatch![1]).not.toBe("251.25410.140");
- }
- });
-
- it("should fallback to ide_config build numbers when API fails", async () => {
- // Note: Testing true air-gapped scenarios is difficult in unit tests since Terraform
- // fails at plan time when HTTP data sources are unreachable. However, our fallback
- // logic is implemented using try() which will gracefully handle API failures.
- // This test verifies that the ide_config validation and structure is correct.
- const customIdeConfig = JSON.stringify({
- CL: {
- name: "CLion",
- icon: "/icon/clion.svg",
- build: "999.fallback.123",
- },
- GO: {
- name: "GoLand",
- icon: "/icon/goland.svg",
- build: "999.fallback.124",
- },
- IU: {
- name: "IntelliJ IDEA",
- icon: "/icon/intellij.svg",
- build: "999.fallback.125",
- },
- PS: {
- name: "PhpStorm",
- icon: "/icon/phpstorm.svg",
- build: "999.fallback.126",
- },
- PY: {
- name: "PyCharm",
- icon: "/icon/pycharm.svg",
- build: "999.fallback.127",
- },
- RD: {
- name: "Rider",
- icon: "/icon/rider.svg",
- build: "999.fallback.128",
- },
- RM: {
- name: "RubyMine",
- icon: "/icon/rubymine.svg",
- build: "999.fallback.129",
- },
- RR: {
- name: "RustRover",
- icon: "/icon/rustrover.svg",
- build: "999.fallback.130",
- },
- WS: {
- name: "WebStorm",
- icon: "/icon/webstorm.svg",
- build: "999.fallback.131",
- },
- });
-
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- ide_config: customIdeConfig,
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
-
- // Should work with custom ide_config (API data will override in connected environments)
- expect(coder_app?.instances[0].attributes.url).toContain(
- "ide_build_number=",
- );
- expect(coder_app?.instances[0].attributes.display_name).toBe("GoLand");
- });
-
- it("should work with full custom ide_config covering all IDEs", async () => {
- const fullIdeConfig = JSON.stringify({
- CL: { name: "CLion", icon: "/icon/clion.svg", build: "999.test.123" },
- GO: { name: "GoLand", icon: "/icon/goland.svg", build: "999.test.124" },
- IU: {
- name: "IntelliJ IDEA",
- icon: "/icon/intellij.svg",
- build: "999.test.125",
- },
- PS: {
- name: "PhpStorm",
- icon: "/icon/phpstorm.svg",
- build: "999.test.126",
- },
- PY: {
- name: "PyCharm",
- icon: "/icon/pycharm.svg",
- build: "999.test.127",
- },
- RD: { name: "Rider", icon: "/icon/rider.svg", build: "999.test.128" },
- RM: {
- name: "RubyMine",
- icon: "/icon/rubymine.svg",
- build: "999.test.129",
- },
- RR: {
- name: "RustRover",
- icon: "/icon/rustrover.svg",
- build: "999.test.130",
- },
- WS: {
- name: "WebStorm",
- icon: "/icon/webstorm.svg",
- build: "999.test.131",
- },
- });
-
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO", "IU", "WS"]',
- ide_config: fullIdeConfig,
- });
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
-
- // Should create apps with custom configuration
- expect(coder_apps.length).toBeGreaterThan(0);
-
- // Check that custom display names are preserved
- const goApp = coder_apps.find(
- (app) => app.instances[0].attributes.slug === "jetbrains-go",
- );
- if (goApp) {
- expect(goApp.instances[0].attributes.display_name).toBe("GoLand");
- expect(goApp.instances[0].attributes.icon).toBe("/icon/goland.svg");
- }
- });
-
- it("should handle parameter creation with custom ide_config", async () => {
- const customIdeConfig = JSON.stringify({
- CL: { name: "CLion", icon: "/icon/clion.svg", build: "999.param.123" },
- GO: {
- name: "GoLand",
- icon: "/icon/goland.svg",
- build: "999.param.124",
- },
- IU: {
- name: "IntelliJ IDEA",
- icon: "/icon/intellij.svg",
- build: "999.param.125",
- },
- PS: {
- name: "PhpStorm",
- icon: "/icon/phpstorm.svg",
- build: "999.param.126",
- },
- PY: {
- name: "PyCharm",
- icon: "/icon/pycharm.svg",
- build: "999.param.127",
- },
- RD: { name: "Rider", icon: "/icon/rider.svg", build: "999.param.128" },
- RM: {
- name: "RubyMine",
- icon: "/icon/rubymine.svg",
- build: "999.param.129",
- },
- RR: {
- name: "RustRover",
- icon: "/icon/rustrover.svg",
- build: "999.param.130",
- },
- WS: {
- name: "WebStorm",
- icon: "/icon/webstorm.svg",
- build: "999.param.131",
- },
- });
-
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- options: '["GO", "IU"]',
- ide_config: customIdeConfig,
- });
-
- // Should create parameter with custom configuration
- const parameter = state.resources.find(
- (res) =>
- res.type === "coder_parameter" && res.name === "jetbrains_ides",
- );
- expect(parameter).toBeDefined();
- expect(parameter?.instances[0].attributes.option).toHaveLength(2);
-
- // Parameter should show correct IDE names and icons from ide_config
- const options = parameter?.instances[0].attributes.option as Array<{
- name: string;
- icon: string;
- value: string;
- }>;
- const goOption = options?.find((opt) => opt.value === "GO");
- expect(goOption?.name).toBe("GoLand");
- expect(goOption?.icon).toBe("/icon/goland.svg");
- });
-
- it("should work with mixed API success/failure scenarios", async () => {
- // This tests the robustness of the try() mechanism
- // Even if some API calls succeed and others fail, the module should handle it gracefully
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO"]',
- // Use real API endpoint - if it fails, fallback should work
- releases_base_link: "https://data.services.jetbrains.com",
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
-
- // Should create app regardless of API success/failure
- expect(coder_app).toBeDefined();
- expect(coder_app?.instances[0].attributes.url).toContain(
- "ide_build_number=",
- );
- });
-
- it("should preserve custom IDE metadata in air-gapped environments", async () => {
- // This test validates that ide_config structure supports air-gapped deployments
- // by ensuring custom metadata is correctly configured for all default IDEs
- const airGappedIdeConfig = JSON.stringify({
- CL: {
- name: "CLion Enterprise",
- icon: "/enterprise/clion.svg",
- build: "251.air.123",
- },
- GO: {
- name: "GoLand Enterprise",
- icon: "/enterprise/goland.svg",
- build: "251.air.124",
- },
- IU: {
- name: "IntelliJ IDEA Enterprise",
- icon: "/enterprise/intellij.svg",
- build: "251.air.125",
- },
- PS: {
- name: "PhpStorm Enterprise",
- icon: "/enterprise/phpstorm.svg",
- build: "251.air.126",
- },
- PY: {
- name: "PyCharm Enterprise",
- icon: "/enterprise/pycharm.svg",
- build: "251.air.127",
- },
- RD: {
- name: "Rider Enterprise",
- icon: "/enterprise/rider.svg",
- build: "251.air.128",
- },
- RM: {
- name: "RubyMine Enterprise",
- icon: "/enterprise/rubymine.svg",
- build: "251.air.129",
- },
- RR: {
- name: "RustRover Enterprise",
- icon: "/enterprise/rustrover.svg",
- build: "251.air.130",
- },
- WS: {
- name: "WebStorm Enterprise",
- icon: "/enterprise/webstorm.svg",
- build: "251.air.131",
- },
- });
-
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["RR"]',
- ide_config: airGappedIdeConfig,
- });
-
- const coder_app = state.resources.find(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
-
- // Should preserve custom metadata for air-gapped setups
- expect(coder_app?.instances[0].attributes.display_name).toBe(
- "RustRover Enterprise",
- );
- expect(coder_app?.instances[0].attributes.icon).toBe(
- "/enterprise/rustrover.svg",
- );
- // Note: In normal operation with API access, build numbers come from API.
- // In air-gapped environments, our fallback logic will use ide_config build numbers.
- expect(coder_app?.instances[0].attributes.url).toContain(
- "ide_build_number=",
- );
- });
-
- it("should validate that fallback mechanism doesn't break existing functionality", async () => {
- // Regression test to ensure our changes don't break normal operation
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- folder: "/home/coder",
- default: '["GO", "IU"]',
- major_version: "latest",
- channel: "release",
- });
-
- const coder_apps = state.resources.filter(
- (res) => res.type === "coder_app" && res.name === "jetbrains",
- );
-
- // Should work normally with API when available
- expect(coder_apps.length).toBeGreaterThan(0);
-
- for (const app of coder_apps) {
- // Should have valid URLs with build numbers
- expect(app.instances[0].attributes.url).toContain(
- "jetbrains://gateway/coder",
- );
- expect(app.instances[0].attributes.url).toContain("ide_build_number=");
- expect(app.instances[0].attributes.url).toContain("ide_product_code=");
- }
- });
- });
-});
diff --git a/registry/coder/modules/jetbrains/main.tf b/registry/coder/modules/jetbrains/main.tf
index 51f7c816..2fac060f 100644
--- a/registry/coder/modules/jetbrains/main.tf
+++ b/registry/coder/modules/jetbrains/main.tf
@@ -62,7 +62,7 @@ variable "coder_parameter_order" {
variable "tooltip" {
type = string
description = "Markdown text that is displayed when hovering over workspace apps."
- default = null
+ default = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
}
variable "major_version" {
@@ -70,8 +70,8 @@ variable "major_version" {
description = "The major version of the IDE. i.e. 2025.1"
default = "latest"
validation {
- condition = can(regex("^[0-9]{4}\\.[0-2]{1}$", var.major_version)) || var.major_version == "latest"
- error_message = "The major_version must be a valid version number. i.e. 2025.1 or latest"
+ condition = can(regex("^[0-9]{4}\\.[1-3]$", var.major_version)) || var.major_version == "latest"
+ error_message = "The major_version must be a valid version number (e.g., 2025.1) or 'latest'"
}
}
@@ -126,7 +126,7 @@ variable "download_base_link" {
data "http" "jetbrains_ide_versions" {
for_each = length(var.default) == 0 ? var.options : var.default
- url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}&latest=true${var.major_version == "latest" ? "" : "&major_version=${var.major_version}"}"
+ url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}${var.major_version == "latest" ? "&latest=true" : ""}"
}
variable "ide_config" {
@@ -138,9 +138,9 @@ variable "ide_config" {
- build: The build number of the IDE.
Example:
{
- "CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
- "GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
- "IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
+ "CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
+ "GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
+ "IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
}
EOT
type = map(object({
@@ -149,15 +149,15 @@ variable "ide_config" {
build = string
}))
default = {
- "CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
- "GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
- "IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
- "PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
- "PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
- "RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
- "RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
- "RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
- "WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
+ "CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
+ "GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
+ "IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
+ "PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" },
+ "PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" },
+ "RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" },
+ "RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" },
+ "RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" },
+ "WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" }
}
validation {
condition = length(var.ide_config) > 0
@@ -182,6 +182,20 @@ locals {
)
}
+ # Filter the parsed response for the requested major version if not "latest"
+ filtered_releases = {
+ for code in length(var.default) == 0 ? var.options : var.default : code => [
+ for r in try(local.parsed_responses[code][keys(local.parsed_responses[code])[0]], []) :
+ r if var.major_version == "latest" || r.majorVersion == var.major_version
+ ]
+ }
+
+ # Select the latest release for the requested major version (first item in the filtered list)
+ selected_releases = {
+ for code in length(var.default) == 0 ? var.options : var.default : code =>
+ length(local.filtered_releases[code]) > 0 ? local.filtered_releases[code][0] : null
+ }
+
# Dynamically generate IDE configurations based on options with fallback to ide_config
options_metadata = {
for code in length(var.default) == 0 ? var.options : var.default : code => {
@@ -191,13 +205,10 @@ locals {
key = code
# Use API build number if available, otherwise fall back to ide_config build number
- build = length(keys(local.parsed_responses[code])) > 0 ? (
- local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0].build
- ) : var.ide_config[code].build
+ build = local.selected_releases[code] != null ? local.selected_releases[code].build : var.ide_config[code].build
- # Store API data for potential future use (only if API is available)
- json_data = length(keys(local.parsed_responses[code])) > 0 ? local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0] : null
- response_key = length(keys(local.parsed_responses[code])) > 0 ? keys(local.parsed_responses[code])[0] : null
+ # Store API data for potential future use
+ json_data = local.selected_releases[code]
}
}
From 28fc956110b777d6cb88e80be4a6dc02f23b269f Mon Sep 17 00:00:00 2001
From: Yevhenii Shcherbina
Date: Wed, 7 Jan 2026 10:17:40 -0500
Subject: [PATCH 7/8] fix: minor boundary bug in claude-code module (#637)
Removing existing boundary directory to allow re-running the script
safely
---
registry/coder/modules/claude-code/README.md | 14 +++++++-------
.../coder/modules/claude-code/scripts/start.sh | 7 +++++++
2 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md
index 0256e029..9a9c062f 100644
--- a/registry/coder/modules/claude-code/README.md
+++ b/registry/coder/modules/claude-code/README.md
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "4.2.8"
+ version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
@@ -45,7 +45,7 @@ This example shows how to configure the Claude Code module to run the agent behi
```tf
module "claude-code" {
source = "dev.registry.coder.com/coder/claude-code/coder"
- version = "4.2.8"
+ version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_boundary = true
@@ -72,7 +72,7 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "4.2.8"
+ version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
@@ -108,7 +108,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "4.2.8"
+ version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
install_claude_code = true
@@ -130,7 +130,7 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "4.2.8"
+ version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
@@ -203,7 +203,7 @@ resource "coder_env" "bedrock_api_key" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "4.2.8"
+ version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
@@ -260,7 +260,7 @@ resource "coder_env" "google_application_credentials" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "4.2.8"
+ version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh
index ebca736c..061dce67 100644
--- a/registry/coder/modules/claude-code/scripts/start.sh
+++ b/registry/coder/modules/claude-code/scripts/start.sh
@@ -48,6 +48,13 @@ function install_boundary() {
if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then
# Install boundary by compiling from source
echo "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)"
+
+ echo "Removing existing boundary directory to allow re-running the script safely"
+ if [ -d boundary ]; then
+ rm -rf boundary
+ fi
+
+ echo "Clone boundary repository"
git clone https://github.com/coder/boundary.git
cd boundary
git checkout "$ARG_BOUNDARY_VERSION"
From 2c4cd861309ae916e5acf8d145628bede68e2f72 Mon Sep 17 00:00:00 2001
From: Atif Ali
Date: Thu, 8 Jan 2026 00:19:12 +0500
Subject: [PATCH 8/8] feat(mux): update logo (#638)
---
.icons/mux.svg | 52 +++-------------------------
registry/coder/modules/mux/README.md | 12 +++----
2 files changed, 11 insertions(+), 53 deletions(-)
diff --git a/.icons/mux.svg b/.icons/mux.svg
index 95b56bb0..a6ce26f1 100644
--- a/.icons/mux.svg
+++ b/.icons/mux.svg
@@ -1,47 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md
index 4df26ce7..3f55209e 100644
--- a/registry/coder/modules/mux/README.md
+++ b/registry/coder/modules/mux/README.md
@@ -14,7 +14,7 @@ Automatically install and run [mux](https://github.com/coder/mux) in a Coder wor
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
- version = "1.0.6"
+ version = "1.0.7"
agent_id = coder_agent.main.id
}
```
@@ -37,7 +37,7 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
- version = "1.0.6"
+ version = "1.0.7"
agent_id = coder_agent.main.id
}
```
@@ -48,7 +48,7 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
- version = "1.0.6"
+ version = "1.0.7"
agent_id = coder_agent.main.id
# Default is "latest"; set to a specific version to pin
install_version = "0.4.0"
@@ -61,7 +61,7 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
- version = "1.0.6"
+ version = "1.0.7"
agent_id = coder_agent.main.id
port = 8080
}
@@ -75,7 +75,7 @@ Run an existing copy of mux if found, otherwise install from npm:
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
- version = "1.0.6"
+ version = "1.0.7"
agent_id = coder_agent.main.id
use_cached = true
}
@@ -89,7 +89,7 @@ Run without installing from the network (requires mux to be pre-installed):
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
- version = "1.0.6"
+ version = "1.0.7"
agent_id = coder_agent.main.id
install = false
}