From c390ed005f7394126b9d27431a05e89b8ed4660b Mon Sep 17 00:00:00 2001 From: Guspan Tanadi <36249910+guspan-tanadi@users.noreply.github.com> Date: Wed, 12 Feb 2025 19:58:07 +0700 Subject: [PATCH 01/41] docs: update section links JSON Settings (#401) --- code-server/README.md | 2 +- vscode-web/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code-server/README.md b/code-server/README.md index 40fbb788..1121b1b8 100644 --- a/code-server/README.md +++ b/code-server/README.md @@ -56,7 +56,7 @@ Enter the `.` into the extensions array and code-server will autom ### Pre-configure Settings -Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file: +Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file) file: ```tf module "code-server" { diff --git a/vscode-web/README.md b/vscode-web/README.md index 7d22feaa..d928205a 100644 --- a/vscode-web/README.md +++ b/vscode-web/README.md @@ -54,7 +54,7 @@ module "vscode-web" { ### Pre-configure Settings -Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file: +Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file) file: ```tf module "vscode-web" { From 3d33656bccdf58069e901324dbedae1e50938965 Mon Sep 17 00:00:00 2001 From: Roger Chao Date: Thu, 13 Feb 2025 20:49:19 -0800 Subject: [PATCH 02/41] feat(vscode-web): allow pinning vscode-web binary to a specific commit ID (#402) Adds support for specifying a commit ID to pin the vscode-web binary version in the module. --- vscode-web/README.md | 8 ++++---- vscode-web/main.tf | 7 +++++++ vscode-web/run.sh | 11 +++++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/vscode-web/README.md b/vscode-web/README.md index d928205a..a5a4b0d9 100644 --- a/vscode-web/README.md +++ b/vscode-web/README.md @@ -15,7 +15,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/ module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.29" + version = "1.0.30" agent_id = coder_agent.example.id accept_license = true } @@ -31,7 +31,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.29" + version = "1.0.30" agent_id = coder_agent.example.id install_prefix = "/home/coder/.vscode-web" folder = "/home/coder" @@ -45,7 +45,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.29" + version = "1.0.30" agent_id = coder_agent.example.id extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"] accept_license = true @@ -60,7 +60,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.29" + version = "1.0.30" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { diff --git a/vscode-web/main.tf b/vscode-web/main.tf index 4a2f04ea..3b1a4efb 100644 --- a/vscode-web/main.tf +++ b/vscode-web/main.tf @@ -59,6 +59,12 @@ variable "install_prefix" { default = "/tmp/vscode-web" } +variable "vscode_web_commit_id" { + type = string + description = "Specify the commit ID of the VS Code Web binary to pin to a specific version. If left empty, the latest stable version is used." + default = "" +} + variable "extensions" { type = list(string) description = "A list of extensions to install." @@ -151,6 +157,7 @@ resource "coder_script" "vscode-web" { FOLDER : var.folder, AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions, SERVER_BASE_PATH : local.server_base_path, + VSCODE_WEB_COMMIT_ID : var.vscode_web_commit_id, }) run_on_start = true diff --git a/vscode-web/run.sh b/vscode-web/run.sh index c3423dff..03927519 100755 --- a/vscode-web/run.sh +++ b/vscode-web/run.sh @@ -59,8 +59,15 @@ case "$ARCH" in ;; esac -HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2) -output=$(curl -fsSL https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-linux-$ARCH-web.tar.gz | tar -xz -C ${INSTALL_PREFIX} --strip-components 1) +# Check if a specific VS Code Web commit ID was provided +if [ -n "${VSCODE_WEB_COMMIT_ID}" ]; then + HASH="${VSCODE_WEB_COMMIT_ID}" +else + HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2) +fi +printf "$${BOLD}VS Code Web commit id version $HASH.\n" + +output=$(curl -fsSL "https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-linux-$ARCH-web.tar.gz" | tar -xz -C "${INSTALL_PREFIX}" --strip-components 1) if [ $? -ne 0 ]; then echo "Failed to install Microsoft Visual Studio Code Server: $output" From 4d2531548f58f447513bf17f4f217e72d28236e8 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Sat, 15 Feb 2025 01:09:04 +0500 Subject: [PATCH 03/41] Revert "feat(vscode-web): allow pinning vscode-web binary to a specific commit ID" (#403) --- vscode-web/README.md | 8 ++++---- vscode-web/main.tf | 7 ------- vscode-web/run.sh | 11 ++--------- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/vscode-web/README.md b/vscode-web/README.md index a5a4b0d9..d928205a 100644 --- a/vscode-web/README.md +++ b/vscode-web/README.md @@ -15,7 +15,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/ module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.30" + version = "1.0.29" agent_id = coder_agent.example.id accept_license = true } @@ -31,7 +31,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.30" + version = "1.0.29" agent_id = coder_agent.example.id install_prefix = "/home/coder/.vscode-web" folder = "/home/coder" @@ -45,7 +45,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.30" + version = "1.0.29" agent_id = coder_agent.example.id extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"] accept_license = true @@ -60,7 +60,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.30" + version = "1.0.29" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { diff --git a/vscode-web/main.tf b/vscode-web/main.tf index 3b1a4efb..4a2f04ea 100644 --- a/vscode-web/main.tf +++ b/vscode-web/main.tf @@ -59,12 +59,6 @@ variable "install_prefix" { default = "/tmp/vscode-web" } -variable "vscode_web_commit_id" { - type = string - description = "Specify the commit ID of the VS Code Web binary to pin to a specific version. If left empty, the latest stable version is used." - default = "" -} - variable "extensions" { type = list(string) description = "A list of extensions to install." @@ -157,7 +151,6 @@ resource "coder_script" "vscode-web" { FOLDER : var.folder, AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions, SERVER_BASE_PATH : local.server_base_path, - VSCODE_WEB_COMMIT_ID : var.vscode_web_commit_id, }) run_on_start = true diff --git a/vscode-web/run.sh b/vscode-web/run.sh index 03927519..c3423dff 100755 --- a/vscode-web/run.sh +++ b/vscode-web/run.sh @@ -59,15 +59,8 @@ case "$ARCH" in ;; esac -# Check if a specific VS Code Web commit ID was provided -if [ -n "${VSCODE_WEB_COMMIT_ID}" ]; then - HASH="${VSCODE_WEB_COMMIT_ID}" -else - HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2) -fi -printf "$${BOLD}VS Code Web commit id version $HASH.\n" - -output=$(curl -fsSL "https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-linux-$ARCH-web.tar.gz" | tar -xz -C "${INSTALL_PREFIX}" --strip-components 1) +HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2) +output=$(curl -fsSL https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-linux-$ARCH-web.tar.gz | tar -xz -C ${INSTALL_PREFIX} --strip-components 1) if [ $? -ne 0 ]; then echo "Failed to install Microsoft Visual Studio Code Server: $output" From 189636c4b0c083f8e2e91b0263f9c32b28d6f84d Mon Sep 17 00:00:00 2001 From: Roger Chao Date: Mon, 17 Feb 2025 16:14:32 -0800 Subject: [PATCH 04/41] feat(vscode-web): allow pinning vscode-web version to a specific commit ID (#405) Adds support for specifying a commit ID to pin the vscode-web version in the module with added example in README. --- vscode-web/README.md | 23 +++++++++++++++++++---- vscode-web/main.tf | 7 +++++++ vscode-web/run.sh | 11 +++++++++-- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/vscode-web/README.md b/vscode-web/README.md index d928205a..5846c04c 100644 --- a/vscode-web/README.md +++ b/vscode-web/README.md @@ -15,7 +15,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/ module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.29" + version = "1.0.30" agent_id = coder_agent.example.id accept_license = true } @@ -31,7 +31,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.29" + version = "1.0.30" agent_id = coder_agent.example.id install_prefix = "/home/coder/.vscode-web" folder = "/home/coder" @@ -45,7 +45,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.29" + version = "1.0.30" agent_id = coder_agent.example.id extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"] accept_license = true @@ -60,7 +60,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.29" + version = "1.0.30" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -69,3 +69,18 @@ module "vscode-web" { accept_license = true } ``` + +### Pin a specific VS Code Web version + +By default, this module installs the latest. To pin a specific version, retrieve the commit ID from the [VS Code Update API](https://update.code.visualstudio.com/api/commits/stable/server-linux-x64-web) and verify its corresponding release on the [VS Code GitHub Releases](https://github.com/microsoft/vscode/releases). + +```tf +module "vscode-web" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/vscode-web/coder" + version = "1.0.30" + agent_id = coder_agent.example.id + commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447" + accept_license = true +} +``` diff --git a/vscode-web/main.tf b/vscode-web/main.tf index 4a2f04ea..11e220cd 100644 --- a/vscode-web/main.tf +++ b/vscode-web/main.tf @@ -59,6 +59,12 @@ variable "install_prefix" { default = "/tmp/vscode-web" } +variable "commit_id" { + type = string + description = "Specify the commit ID of the VS Code Web binary to pin to a specific version. If left empty, the latest stable version is used." + default = "" +} + variable "extensions" { type = list(string) description = "A list of extensions to install." @@ -151,6 +157,7 @@ resource "coder_script" "vscode-web" { FOLDER : var.folder, AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions, SERVER_BASE_PATH : local.server_base_path, + COMMIT_ID : var.commit_id, }) run_on_start = true diff --git a/vscode-web/run.sh b/vscode-web/run.sh index c3423dff..588cec56 100755 --- a/vscode-web/run.sh +++ b/vscode-web/run.sh @@ -59,8 +59,15 @@ case "$ARCH" in ;; esac -HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2) -output=$(curl -fsSL https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-linux-$ARCH-web.tar.gz | tar -xz -C ${INSTALL_PREFIX} --strip-components 1) +# Check if a specific VS Code Web commit ID was provided +if [ -n "${COMMIT_ID}" ]; then + HASH="${COMMIT_ID}" +else + HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2) +fi +printf "$${BOLD}VS Code Web commit id version $HASH.\n" + +output=$(curl -fsSL "https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-linux-$ARCH-web.tar.gz" | tar -xz -C "${INSTALL_PREFIX}" --strip-components 1) if [ $? -ne 0 ]; then echo "Failed to install Microsoft Visual Studio Code Server: $output" From 01d2c10b6a7ad07ce4a92c781c85d48f50b7971b Mon Sep 17 00:00:00 2001 From: joehutcheson <63621767+joehutcheson@users.noreply.github.com> Date: Tue, 18 Feb 2025 20:07:54 +0000 Subject: [PATCH 05/41] feat(jfrog-token): update module to accept username (#408) --- jfrog-token/README.md | 8 ++++---- jfrog-token/main.test.ts | 1 + jfrog-token/main.tf | 10 ++++++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/jfrog-token/README.md b/jfrog-token/README.md index 146dc7f7..ce165222 100644 --- a/jfrog-token/README.md +++ b/jfrog-token/README.md @@ -15,7 +15,7 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti ```tf module "jfrog" { source = "registry.coder.com/modules/jfrog-token/coder" - version = "1.0.19" + version = "1.0.30" agent_id = coder_agent.example.id jfrog_url = "https://XXXX.jfrog.io" artifactory_access_token = var.artifactory_access_token @@ -42,7 +42,7 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat ```tf module "jfrog" { source = "registry.coder.com/modules/jfrog-token/coder" - version = "1.0.19" + version = "1.0.30" agent_id = coder_agent.example.id jfrog_url = "https://YYYY.jfrog.io" artifactory_access_token = var.artifactory_access_token # An admin access token @@ -75,7 +75,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio ```tf module "jfrog" { source = "registry.coder.com/modules/jfrog-token/coder" - version = "1.0.19" + version = "1.0.30" agent_id = coder_agent.example.id jfrog_url = "https://XXXX.jfrog.io" artifactory_access_token = var.artifactory_access_token @@ -95,7 +95,7 @@ data "coder_workspace" "me" {} module "jfrog" { source = "registry.coder.com/modules/jfrog-token/coder" - version = "1.0.19" + version = "1.0.30" agent_id = coder_agent.example.id jfrog_url = "https://XXXX.jfrog.io" artifactory_access_token = var.artifactory_access_token diff --git a/jfrog-token/main.test.ts b/jfrog-token/main.test.ts index 2c856720..4ba2f52d 100644 --- a/jfrog-token/main.test.ts +++ b/jfrog-token/main.test.ts @@ -20,6 +20,7 @@ describe("jfrog-token", async () => { refreshable?: boolean; expires_in?: number; username_field?: string; + username?: string; jfrog_server_id?: string; configure_code_server?: boolean; }; diff --git a/jfrog-token/main.tf b/jfrog-token/main.tf index f6f5f5b0..720e2d8c 100644 --- a/jfrog-token/main.tf +++ b/jfrog-token/main.tf @@ -68,6 +68,12 @@ variable "username_field" { } } +variable "username" { + type = string + description = "Username to use for Artifactory. Overrides the field specified in `username_field`" + default = null +} + variable "agent_id" { type = string description = "The ID of a Coder agent." @@ -99,8 +105,8 @@ variable "package_managers" { } locals { - # The username field to use for artifactory - username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name + # The username to use for artifactory + username = coalesce(var.username, var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name) jfrog_host = split("://", var.jfrog_url)[1] common_values = { JFROG_URL = var.jfrog_url From 0703b13354cf617ac5b8e74349dd4de4a8cd8316 Mon Sep 17 00:00:00 2001 From: ffais <42377700+ffais@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:10:26 +0100 Subject: [PATCH 06/41] feat(jupyterlab): added support for uv as installer (#406) Co-authored-by: M Atif Ali --- jupyterlab/README.md | 2 +- jupyterlab/main.test.ts | 52 +++++++++++++++++++++++++++++++++++++---- jupyterlab/run.sh | 46 ++++++++++++++++++++++++++---------- 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/jupyterlab/README.md b/jupyterlab/README.md index c285c275..8c2af03f 100644 --- a/jupyterlab/README.md +++ b/jupyterlab/README.md @@ -17,7 +17,7 @@ A module that adds JupyterLab in your Coder template. module "jupyterlab" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jupyterlab/coder" - version = "1.0.23" + version = "1.0.30" agent_id = coder_agent.example.id } ``` diff --git a/jupyterlab/main.test.ts b/jupyterlab/main.test.ts index cf9ac1f0..a9789c39 100644 --- a/jupyterlab/main.test.ts +++ b/jupyterlab/main.test.ts @@ -33,6 +33,33 @@ const executeScriptInContainerWithPip = async ( }; }; +// executes the coder script after installing pip +const executeScriptInContainerWithUv = async ( + state: TerraformState, + image: string, + shell = "sh", +): Promise<{ + exitCode: number; + stdout: string[]; + stderr: string[]; +}> => { + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer(image); + const respPipx = await execContainer(id, [ + shell, + "-c", + "apk --no-cache add uv gcc musl-dev linux-headers && uv venv", + ]); + const resp = await execContainer(id, [shell, "-c", instance.script]); + const stdout = resp.stdout.trim().split("\n"); + const stderr = resp.stderr.trim().split("\n"); + return { + exitCode: resp.exitCode, + stdout, + stderr, + }; +}; + describe("jupyterlab", async () => { await runTerraformInit(import.meta.dir); @@ -40,19 +67,36 @@ describe("jupyterlab", async () => { agent_id: "foo", }); - it("fails without pipx", async () => { + it("fails without installers", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", }); const output = await executeScriptInContainer(state, "alpine"); expect(output.exitCode).toBe(1); expect(output.stdout).toEqual([ - "\u001B[0;1mInstalling jupyterlab!", - "pipx is not installed", - "Please install pipx in your Dockerfile/VM image before running this script", + "Checking for a supported installer", + "No valid installer is not installed", + "Please install pipx or uv in your Dockerfile/VM image before running this script", ]); }); + // TODO: Add faster test to run with uv. + // currently times out. + // it("runs with uv", async () => { + // const state = await runTerraformApply(import.meta.dir, { + // agent_id: "foo", + // }); + // const output = await executeScriptInContainerWithUv(state, "python:3-alpine"); + // expect(output.exitCode).toBe(0); + // expect(output.stdout).toEqual([ + // "Checking for a supported installer", + // "uv is installed", + // "\u001B[0;1mInstalling jupyterlab!", + // "đŸĨŗ jupyterlab has been installed", + // "👷 Starting jupyterlab in background...check logs at /tmp/jupyterlab.log", + // ]); + // }); + // TODO: Add faster test to run with pipx. // currently times out. // it("runs with pipx", async () => { diff --git a/jupyterlab/run.sh b/jupyterlab/run.sh index 0c3780dc..2dd34ace 100755 --- a/jupyterlab/run.sh +++ b/jupyterlab/run.sh @@ -1,4 +1,23 @@ #!/usr/bin/env sh +INSTALLER="" +check_available_installer() { + # check if pipx is installed + echo "Checking for a supported installer" + if command -v pipx > /dev/null 2>&1; then + echo "pipx is installed" + INSTALLER="pipx" + return + fi + # check if uv is installed + if command -v uv > /dev/null 2>&1; then + echo "uv is installed" + INSTALLER="uv" + return + fi + echo "No valid installer is not installed" + echo "Please install pipx or uv in your Dockerfile/VM image before running this script" + exit 1 +} if [ -n "${BASE_URL}" ]; then BASE_URL_FLAG="--ServerApp.base_url=${BASE_URL}" @@ -6,27 +25,30 @@ fi BOLD='\033[0;1m' -printf "$${BOLD}Installing jupyterlab!\n" - # check if jupyterlab is installed if ! command -v jupyter-lab > /dev/null 2>&1; then # install jupyterlab - # check if pipx is installed - if ! command -v pipx > /dev/null 2>&1; then - echo "pipx is not installed" - echo "Please install pipx in your Dockerfile/VM image before running this script" - exit 1 - fi - # install jupyterlab - pipx install -q jupyterlab - printf "%s\n\n" "đŸĨŗ jupyterlab has been installed" + check_available_installer + printf "$${BOLD}Installing jupyterlab!\n" + case $INSTALLER in + uv) + uv pip install -q jupyterlab \ + && printf "%s\n" "đŸĨŗ jupyterlab has been installed" + JUPYTERPATH="$HOME/.venv/bin/" + ;; + pipx) + pipx install jupyterlab \ + && printf "%s\n" "đŸĨŗ jupyterlab has been installed" + JUPYTERPATH="$HOME/.local/bin" + ;; + esac else printf "%s\n\n" "đŸĨŗ jupyterlab is already installed" fi printf "👷 Starting jupyterlab in background..." printf "check logs at ${LOG_PATH}" -$HOME/.local/bin/jupyter-lab --no-browser \ +$JUPYTERPATH/jupyter-lab --no-browser \ "$BASE_URL_FLAG" \ --ServerApp.ip='*' \ --ServerApp.port="${PORT}" \ From fd5dd375f7f8740226e798fc60a4a5d271b294d4 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Tue, 11 Mar 2025 14:06:44 +0500 Subject: [PATCH 07/41] docs: fix the linked GitHub repo URL in cursor module (#413) --- cursor/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cursor/README.md b/cursor/README.md index 46d73b5c..d9a2e17f 100644 --- a/cursor/README.md +++ b/cursor/README.md @@ -11,7 +11,7 @@ tags: [ide, cursor, helper] Add a button to open any workspace with a single click in Cursor IDE. -Uses the [Coder Remote VS Code Extension](https://github.com/coder/cursor-coder). +Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder). ```tf module "cursor" { From 3caf1b20cb795ca043a6ae16051d50d4c0ce1291 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 21 Mar 2025 03:37:23 +0200 Subject: [PATCH 08/41] feat(code-server): link binary to PATH (#415) Noticed we don't place `code-server` in path, so made a quick fix. --- code-server/README.md | 14 +++++++------- code-server/run.sh | 10 ++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/code-server/README.md b/code-server/README.md index 1121b1b8..e7098113 100644 --- a/code-server/README.md +++ b/code-server/README.md @@ -15,7 +15,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id } ``` @@ -30,7 +30,7 @@ module "code-server" { module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id install_version = "4.8.3" } @@ -44,7 +44,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/): module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id extensions = [ "dracula-theme.theme-dracula" @@ -62,7 +62,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -79,7 +79,7 @@ Just run code-server in the background, don't fetch it from GitHub: module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] } @@ -95,7 +95,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub: module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id use_cached = true extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] @@ -108,7 +108,7 @@ Just run code-server in the background, don't fetch it from GitHub: module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id offline = true } diff --git a/code-server/run.sh b/code-server/run.sh index 457cff7f..99b30c0e 100755 --- a/code-server/run.sh +++ b/code-server/run.sh @@ -42,6 +42,11 @@ fi if [ ! -f "$CODE_SERVER" ] || [ "${USE_CACHED}" != true ]; then printf "$${BOLD}Installing code-server!\n" + # Clean up from other install (in case install prefix changed). + if [ -n "$CODER_SCRIPT_BIN_DIR" ] && [ -e "$CODER_SCRIPT_BIN_DIR/code-server" ]; then + rm "$CODER_SCRIPT_BIN_DIR/code-server" + fi + ARGS=( "--method=standalone" "--prefix=${INSTALL_PREFIX}" @@ -58,6 +63,11 @@ if [ ! -f "$CODE_SERVER" ] || [ "${USE_CACHED}" != true ]; then printf "đŸĨŗ code-server has been installed in ${INSTALL_PREFIX}\n\n" fi +# Make the code-server available in PATH. +if [ -n "$CODER_SCRIPT_BIN_DIR" ] && [ ! -e "$CODER_SCRIPT_BIN_DIR/code-server" ]; then + ln -s "$CODE_SERVER" "$CODER_SCRIPT_BIN_DIR/code-server" +fi + # Get the list of installed extensions... LIST_EXTENSIONS=$($CODE_SERVER --list-extensions $EXTENSION_ARG) readarray -t EXTENSIONS_ARRAY <<< "$LIST_EXTENSIONS" From 8b184eb86d447f36c1971424e8638a1b1303313e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Mar 2025 22:57:00 -0700 Subject: [PATCH 09/41] fix(filebrowser): configure before running (#400) Fixes #399 Instead of trying to cram the config into one command we check if the db exists, if not initialize it then configure the various values before starting the app. --------- Co-authored-by: M Atif Ali --- filebrowser/run.sh | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/filebrowser/run.sh b/filebrowser/run.sh index d6afea02..b8927582 100644 --- a/filebrowser/run.sh +++ b/filebrowser/run.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BOLD='\033[0;1m' +BOLD='\033[[0;1m' printf "$${BOLD}Installing filebrowser \n\n" @@ -11,20 +11,27 @@ fi printf "đŸĨŗ Installation complete! \n\n" -printf "👷 Starting filebrowser in background... \n\n" +printf "đŸ› ī¸ Configuring filebrowser \n\n" ROOT_DIR=${FOLDER} ROOT_DIR=$${ROOT_DIR/\~/$HOME} -DB_FLAG="" -if [ "${DB_PATH}" != "filebrowser.db" ]; then - DB_FLAG=" -d ${DB_PATH}" +echo "DB_PATH: ${DB_PATH}" + +export FB_DATABASE="${DB_PATH}" + +# Check if filebrowser db exists +if [[ ! -f "${DB_PATH}" ]]; then + filebrowser config init 2>&1 | tee -a ${LOG_PATH} + filebrowser users add admin "" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH} fi -printf "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n" +filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH} -printf "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} --baseurl ${SERVER_BASE_PATH}' \n\n" +printf "👷 Starting filebrowser in background... \n\n" + +printf "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n" -filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} --baseurl ${SERVER_BASE_PATH} > ${LOG_PATH} 2>&1 & +filebrowser >> ${LOG_PATH} 2>&1 & printf "📝 Logs at ${LOG_PATH} \n\n" From d0c1657f2221a5ca0f244c07c7298cc07e9b6130 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Tue, 1 Apr 2025 20:00:04 -0500 Subject: [PATCH 10/41] feat: add claude code and Goose module (#420) ready --- .icons/claude.svg | 4 + .icons/goose.svg | 4 + claude-code/README.md | 112 +++++++++++++++++++++ claude-code/main.tf | 170 ++++++++++++++++++++++++++++++++ filebrowser/README.md | 8 +- filebrowser/main.test.ts | 114 --------------------- filebrowser/run.sh | 6 +- goose/README.md | 125 +++++++++++++++++++++++ goose/main.tf | 207 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 629 insertions(+), 121 deletions(-) create mode 100644 .icons/claude.svg create mode 100644 .icons/goose.svg create mode 100644 claude-code/README.md create mode 100644 claude-code/main.tf delete mode 100644 filebrowser/main.test.ts create mode 100644 goose/README.md create mode 100644 goose/main.tf diff --git a/.icons/claude.svg b/.icons/claude.svg new file mode 100644 index 00000000..998fb0d5 --- /dev/null +++ b/.icons/claude.svg @@ -0,0 +1,4 @@ + + + + diff --git a/.icons/goose.svg b/.icons/goose.svg new file mode 100644 index 00000000..cbbe8419 --- /dev/null +++ b/.icons/goose.svg @@ -0,0 +1,4 @@ + + + + diff --git a/claude-code/README.md b/claude-code/README.md new file mode 100644 index 00000000..2422f174 --- /dev/null +++ b/claude-code/README.md @@ -0,0 +1,112 @@ +--- +display_name: Claude Code +description: Run Claude Code in your workspace +icon: ../.icons/claude.svg +maintainer_github: coder +verified: true +tags: [agent, claude-code] +--- + +# Claude Code + +Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks. + +```tf +module "claude-code" { + source = "registry.coder.com/modules/claude-code/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_claude_code = true + claude_code_version = "latest" +} +``` + +### Prerequisites + +- Node.js and npm must be installed in your workspace to install Claude Code +- `screen` must be installed in your workspace to run Claude Code in the background +- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template + +The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces. + +## Examples + +### Run in the background and report tasks (Experimental) + +> This functionality is in early access and subject to change. Do not run in +> production as it is unstable. Instead, deploy these changes into a demo or +> staging environment. +> +> Join our [Discord channel](https://discord.gg/coder) or +> [contact us](https://coder.com/contact) to get help or share feedback. + +Your workspace must have `screen` installed to use this. + +```tf +variable "anthropic_api_key" { + type = string + description = "The Anthropic API key" + sensitive = true +} + +module "coder-login" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/coder-login/coder" + version = "1.0.31" + agent_id = coder_agent.example.id +} + +data "coder_parameter" "ai_prompt" { + type = "string" + name = "AI Prompt" + default = "" + description = "Write a prompt for Claude Code" + mutable = true +} + +# Set the prompt and system prompt for Claude Code via environment variables +resource "coder_agent" "main" { + # ... + env = { + CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter + CODER_MCP_CLAUDE_TASK_PROMPT = data.coder_parameter.ai_prompt.value + CODER_MCP_APP_STATUS_SLUG = "claude-code" + CODER_MCP_CLAUDE_SYSTEM_PROMPT = <<-EOT + You are a helpful assistant that can help with code. + EOT + } +} + +module "claude-code" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/claude-code/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_claude_code = true + claude_code_version = "0.2.57" + + # Enable experimental features + experiment_use_screen = true + experiment_report_tasks = true +} +``` + +## Run standalone + +Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it directly without using screen or any task reporting to the Coder UI. + +```tf +module "claude-code" { + source = "registry.coder.com/modules/claude-code/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_claude_code = true + claude_code_version = "latest" + + # Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL + icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png" +} +``` diff --git a/claude-code/main.tf b/claude-code/main.tf new file mode 100644 index 00000000..349af17f --- /dev/null +++ b/claude-code/main.tf @@ -0,0 +1,170 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.17" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +data "coder_workspace" "me" {} + +data "coder_workspace_owner" "me" {} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +variable "icon" { + type = string + description = "The icon to use for the app." + default = "/icon/claude.svg" +} + +variable "folder" { + type = string + description = "The folder to run Claude Code in." + default = "/home/coder" +} + +variable "install_claude_code" { + type = bool + description = "Whether to install Claude Code." + default = true +} + +variable "claude_code_version" { + type = string + description = "The version of Claude Code to install." + default = "latest" +} + +variable "experiment_use_screen" { + type = bool + description = "Whether to use screen for running Claude Code in the background." + default = false +} + +variable "experiment_report_tasks" { + type = bool + description = "Whether to enable task reporting." + default = false +} + +# Install and Initialize Claude Code +resource "coder_script" "claude_code" { + agent_id = var.agent_id + display_name = "Claude Code" + icon = var.icon + script = <<-EOT + #!/bin/bash + set -e + + # Function to check if a command exists + command_exists() { + command -v "$1" >/dev/null 2>&1 + } + + # Install Claude Code if enabled + if [ "${var.install_claude_code}" = "true" ]; then + if ! command_exists npm; then + echo "Error: npm is not installed. Please install Node.js and npm first." + exit 1 + fi + echo "Installing Claude Code..." + npm install -g @anthropic-ai/claude-code@${var.claude_code_version} + fi + + if [ "${var.experiment_report_tasks}" = "true" ]; then + echo "Configuring Claude Code to report tasks via Coder MCP..." + coder exp mcp configure claude-code ${var.folder} + fi + + # Run with screen if enabled + if [ "${var.experiment_use_screen}" = "true" ]; then + echo "Running Claude Code in the background..." + + # Check if screen is installed + if ! command_exists screen; then + echo "Error: screen is not installed. Please install screen manually." + exit 1 + fi + + touch "$HOME/.claude-code.log" + + # Ensure the screenrc exists + if [ ! -f "$HOME/.screenrc" ]; then + echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.claude-code.log" + echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc" + fi + + if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then + echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log" + echo "multiuser on" >> "$HOME/.screenrc" + fi + + if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then + echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log" + echo "acladd $(whoami)" >> "$HOME/.screenrc" + fi + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + screen -U -dmS claude-code bash -c ' + cd ${var.folder} + claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log" + exec bash + ' + # Extremely hacky way to send the prompt to the screen session + # This will be fixed in the future, but `claude` was not sending MCP + # tasks when an initial prompt is provided. + screen -S claude-code -X stuff "$CODER_MCP_CLAUDE_TASK_PROMPT" + sleep 5 + screen -S claude-code -X stuff "^M" + else + # Check if claude is installed before running + if ! command_exists claude; then + echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually." + exit 1 + fi + fi + EOT + run_on_start = true +} + +resource "coder_app" "claude_code" { + slug = "claude-code" + display_name = "Claude Code" + agent_id = var.agent_id + command = <<-EOT + #!/bin/bash + set -e + + if [ "${var.experiment_use_screen}" = "true" ]; then + if screen -list | grep -q "claude-code"; then + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + echo "Attaching to existing Claude Code session." | tee -a "$HOME/.claude-code.log" + screen -xRR claude-code + else + echo "Starting a new Claude Code session." | tee -a "$HOME/.claude-code.log" + screen -S claude-code bash -c 'export LANG=en_US.UTF-8; export LC_ALL=en_US.UTF-8; claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash' + fi + else + cd ${var.folder} + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + claude + fi + EOT + icon = var.icon +} diff --git a/filebrowser/README.md b/filebrowser/README.md index 13e06577..3a0e56bd 100644 --- a/filebrowser/README.md +++ b/filebrowser/README.md @@ -15,7 +15,7 @@ A file browser for your workspace. module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id } ``` @@ -30,7 +30,7 @@ module "filebrowser" { module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id folder = "/home/coder/project" } @@ -42,7 +42,7 @@ module "filebrowser" { module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id database_path = ".config/filebrowser.db" } @@ -54,7 +54,7 @@ module "filebrowser" { module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id agent_name = "main" subdomain = false diff --git a/filebrowser/main.test.ts b/filebrowser/main.test.ts deleted file mode 100644 index 368075b2..00000000 --- a/filebrowser/main.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { - executeScriptInContainer, - runTerraformApply, - runTerraformInit, - testRequiredVariables, -} from "../test"; - -describe("filebrowser", async () => { - await runTerraformInit(import.meta.dir); - - testRequiredVariables(import.meta.dir, { - agent_id: "foo", - }); - - it("fails with wrong database_path", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - database_path: "nofb", - }).catch((e) => { - if (!e.message.startsWith("\nError: Invalid value for variable")) { - throw e; - } - }); - }); - - it("runs with default", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - }); - const output = await executeScriptInContainer(state, "alpine"); - expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ - "\u001b[0;1mInstalling filebrowser ", - "", - "đŸĨŗ Installation complete! ", - "", - "👷 Starting filebrowser in background... ", - "", - "📂 Serving /root at http://localhost:13339 ", - "", - "Running 'filebrowser --noauth --root /root --port 13339 --baseurl ' ", - "", - "📝 Logs at /tmp/filebrowser.log", - ]); - }); - - it("runs with database_path var", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - database_path: ".config/filebrowser.db", - }); - const output = await executeScriptInContainer(state, "alpine"); - expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ - "\u001b[0;1mInstalling filebrowser ", - "", - "đŸĨŗ Installation complete! ", - "", - "👷 Starting filebrowser in background... ", - "", - "📂 Serving /root at http://localhost:13339 ", - "", - "Running 'filebrowser --noauth --root /root --port 13339 -d .config/filebrowser.db --baseurl ' ", - "", - "📝 Logs at /tmp/filebrowser.log", - ]); - }); - - it("runs with folder var", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - folder: "/home/coder/project", - }); - const output = await executeScriptInContainer(state, "alpine"); - expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ - "\u001b[0;1mInstalling filebrowser ", - "", - "đŸĨŗ Installation complete! ", - "", - "👷 Starting filebrowser in background... ", - "", - "📂 Serving /home/coder/project at http://localhost:13339 ", - "", - "Running 'filebrowser --noauth --root /home/coder/project --port 13339 --baseurl ' ", - "", - "📝 Logs at /tmp/filebrowser.log", - ]); - }); - - it("runs with subdomain=false", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - agent_name: "main", - subdomain: false, - }); - const output = await executeScriptInContainer(state, "alpine"); - expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ - "\u001B[0;1mInstalling filebrowser ", - "", - "đŸĨŗ Installation complete! ", - "", - "👷 Starting filebrowser in background... ", - "", - "📂 Serving /root at http://localhost:13339 ", - "", - "Running 'filebrowser --noauth --root /root --port 13339 --baseurl /@default/default.main/apps/filebrowser' ", - "", - "📝 Logs at /tmp/filebrowser.log", - ]); - }); -}); diff --git a/filebrowser/run.sh b/filebrowser/run.sh index b8927582..84810e4e 100644 --- a/filebrowser/run.sh +++ b/filebrowser/run.sh @@ -22,11 +22,11 @@ export FB_DATABASE="${DB_PATH}" # Check if filebrowser db exists if [[ ! -f "${DB_PATH}" ]]; then - filebrowser config init 2>&1 | tee -a ${LOG_PATH} - filebrowser users add admin "" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH} + filebrowser config init 2>&1 | tee -a ${LOG_PATH} + filebrowser users add admin "" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH} fi -filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH} +filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH} printf "👷 Starting filebrowser in background... \n\n" diff --git a/goose/README.md b/goose/README.md new file mode 100644 index 00000000..5735cb76 --- /dev/null +++ b/goose/README.md @@ -0,0 +1,125 @@ +--- +display_name: Goose +description: Run Goose in your workspace +icon: ../.icons/goose.svg +maintainer_github: coder +verified: true +tags: [agent, goose] +--- + +# Goose + +Run the [Goose](https://block.github.io/goose/) agent in your workspace to generate code and perform tasks. + +```tf +module "goose" { + source = "registry.coder.com/modules/goose/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_goose = true + goose_version = "v1.0.16" +} +``` + +### Prerequisites + +- `screen` must be installed in your workspace to run Goose in the background +- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template + +The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces. + +## Examples + +Your workspace must have `screen` installed to use this. + +### Run in the background and report tasks (Experimental) + +> This functionality is in early access and subject to change. Do not run in +> production as it is unstable. Instead, deploy these changes into a demo or +> staging environment. +> +> Join our [Discord channel](https://discord.gg/coder) or +> [contact us](https://coder.com/contact) to get help or share feedback. + +```tf +module "coder-login" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/coder-login/coder" + version = "1.0.31" + agent_id = coder_agent.example.id +} + +variable "anthropic_api_key" { + type = string + description = "The Anthropic API key" + sensitive = true +} + +data "coder_parameter" "ai_prompt" { + type = "string" + name = "AI Prompt" + default = "" + description = "Write a prompt for Goose" + mutable = true +} + +# Set the prompt and system prompt for Goose via environment variables +resource "coder_agent" "main" { + # ... + env = { + GOOSE_SYSTEM_PROMPT = <<-EOT + You are a helpful assistant that can help write code. + + Run all long running tasks (e.g. npm run dev) in the background and not in the foreground. + + Periodically check in on background tasks. + + Notify Coder of the status of the task before and after your steps. + EOT + GOOSE_TASK_PROMPT = data.coder_parameter.ai_prompt.value + + # An API key is required for experiment_auto_configure + # See https://block.github.io/goose/docs/getting-started/providers + ANTHROPIC_API_KEY = var.anthropic_api_key # or use a coder_parameter + } +} + +module "goose" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/goose/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_goose = true + goose_version = "v1.0.16" + + # Enable experimental features + experiment_report_tasks = true + + # Avoid configuring Goose manually + experiment_auto_configure = true + + # Required for experiment_auto_configure + experiment_goose_provider = "anthropic" + experiment_goose_model = "claude-3-5-sonnet-latest" +} +``` + +## Run standalone + +Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or any task reporting to the Coder UI. + +```tf +module "goose" { + source = "registry.coder.com/modules/goose/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_goose = true + goose_version = "v1.0.16" + + # Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL + icon = "https://raw.githubusercontent.com/block/goose/refs/heads/main/ui/desktop/src/images/icon.svg" +} +``` diff --git a/goose/main.tf b/goose/main.tf new file mode 100644 index 00000000..fcb6baaa --- /dev/null +++ b/goose/main.tf @@ -0,0 +1,207 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.17" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +data "coder_workspace" "me" {} + +data "coder_workspace_owner" "me" {} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +variable "icon" { + type = string + description = "The icon to use for the app." + default = "/icon/goose.svg" +} + +variable "folder" { + type = string + description = "The folder to run Goose in." + default = "/home/coder" +} + +variable "install_goose" { + type = bool + description = "Whether to install Goose." + default = true +} + +variable "goose_version" { + type = string + description = "The version of Goose to install." + default = "stable" +} + +variable "experiment_use_screen" { + type = bool + description = "Whether to use screen for running Goose in the background." + default = false +} + +variable "experiment_report_tasks" { + type = bool + description = "Whether to enable task reporting." + default = false +} + +variable "experiment_auto_configure" { + type = bool + description = "Whether to automatically configure Goose." + default = false +} + +variable "experiment_goose_provider" { + type = string + description = "The provider to use for Goose (e.g., anthropic)." + default = null +} + +variable "experiment_goose_model" { + type = string + description = "The model to use for Goose (e.g., claude-3-5-sonnet-latest)." + default = null +} + +# Install and Initialize Goose +resource "coder_script" "goose" { + agent_id = var.agent_id + display_name = "Goose" + icon = var.icon + script = <<-EOT + #!/bin/bash + set -e + + # Function to check if a command exists + command_exists() { + command -v "$1" >/dev/null 2>&1 + } + + # Install Goose if enabled + if [ "${var.install_goose}" = "true" ]; then + if ! command_exists npm; then + echo "Error: npm is not installed. Please install Node.js and npm first." + exit 1 + fi + echo "Installing Goose..." + RELEASE_TAG=v${var.goose_version} curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash + fi + + # Configure Goose if auto-configure is enabled + if [ "${var.experiment_auto_configure}" = "true" ]; then + echo "Configuring Goose..." + mkdir -p "$HOME/.config/goose" + cat > "$HOME/.config/goose/config.yaml" << EOL +GOOSE_PROVIDER: ${var.experiment_goose_provider} +GOOSE_MODEL: ${var.experiment_goose_model} +extensions: + coder: + args: + - exp + - mcp + - server + cmd: coder + description: Report ALL tasks and statuses (in progress, done, failed) before and after starting + enabled: true + envs: + CODER_MCP_APP_STATUS_SLUG: goose + name: Coder + timeout: 3000 + type: stdio + developer: + display_name: Developer + enabled: true + name: developer + timeout: 300 + type: builtin +EOL + fi + + # Run with screen if enabled + if [ "${var.experiment_use_screen}" = "true" ]; then + echo "Running Goose in the background..." + + # Check if screen is installed + if ! command_exists screen; then + echo "Error: screen is not installed. Please install screen manually." + exit 1 + fi + + touch "$HOME/.goose.log" + + # Ensure the screenrc exists + if [ ! -f "$HOME/.screenrc" ]; then + echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.goose.log" + echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc" + fi + + if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then + echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.goose.log" + echo "multiuser on" >> "$HOME/.screenrc" + fi + + if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then + echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.goose.log" + echo "acladd $(whoami)" >> "$HOME/.screenrc" + fi + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + screen -U -dmS goose bash -c ' + cd ${var.folder} + $HOME/.local/bin/goose run --text "$GOOSE_SYSTEM_PROMPT. Your task: $GOOSE_TASK_PROMPT" --interactive | tee -a "$HOME/.goose.log" + exec bash + ' + else + # Check if goose is installed before running + if ! command_exists $HOME/.local/bin/goose; then + echo "Error: Goose is not installed. Please enable install_goose or install it manually." + exit 1 + fi + fi + EOT + run_on_start = true +} + +resource "coder_app" "goose" { + slug = "goose" + display_name = "Goose" + agent_id = var.agent_id + command = <<-EOT + #!/bin/bash + set -e + + if [ "${var.experiment_use_screen}" = "true" ]; then + if screen -list | grep -q "goose"; then + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + echo "Attaching to existing Goose session." | tee -a "$HOME/.goose.log" + screen -xRR goose + else + echo "Starting a new Goose session." | tee -a "$HOME/.goose.log" + screen -S goose bash -c 'export LANG=en_US.UTF-8; export LC_ALL=en_US.UTF-8; $HOME/.local/bin/goose run --text "Always report status and instructions to Coder, before and after your steps" --interactive | tee -a "$HOME/.goose.log"; exec bash' + fi + else + cd ${var.folder} + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + $HOME/.local/bin/goose + fi + EOT + icon = var.icon +} From be91e18d53fc7c1f0a5c503fff77457c37fe1f90 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Wed, 2 Apr 2025 07:40:39 -0500 Subject: [PATCH 11/41] chore: add more context around experimental features (#421) --- claude-code/README.md | 8 +++++--- goose/README.md | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/claude-code/README.md b/claude-code/README.md index 2422f174..4b7dffbc 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -34,9 +34,11 @@ The `codercom/oss-dogfood:latest` container image can be used for testing on con ### Run in the background and report tasks (Experimental) -> This functionality is in early access and subject to change. Do not run in -> production as it is unstable. Instead, deploy these changes into a demo or -> staging environment. +> This functionality is in early access as of Coder v2.21 and is subject to change. +> Do not run in production as it is unstable. +> Instead, deploy these changes into a demo or staging environment. +> +> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents) > > Join our [Discord channel](https://discord.gg/coder) or > [contact us](https://coder.com/contact) to get help or share feedback. diff --git a/goose/README.md b/goose/README.md index 5735cb76..ed5e09ed 100644 --- a/goose/README.md +++ b/goose/README.md @@ -35,9 +35,11 @@ Your workspace must have `screen` installed to use this. ### Run in the background and report tasks (Experimental) -> This functionality is in early access and subject to change. Do not run in -> production as it is unstable. Instead, deploy these changes into a demo or -> staging environment. +> This functionality is in early access as of Coder v2.21 and is subject to change. +> Do not run in production as it is unstable. +> Instead, deploy these changes into a demo or staging environment. +> +> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents) > > Join our [Discord channel](https://discord.gg/coder) or > [contact us](https://coder.com/contact) to get help or share feedback. From b4569c21f72fb11b874991c63cb8ef4544312bef Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Wed, 2 Apr 2025 07:52:50 -0500 Subject: [PATCH 12/41] chore: soften disclaimers in module docs (#422) --- claude-code/README.md | 6 +++--- goose/README.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/claude-code/README.md b/claude-code/README.md index 4b7dffbc..c599f658 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -34,9 +34,9 @@ The `codercom/oss-dogfood:latest` container image can be used for testing on con ### Run in the background and report tasks (Experimental) -> This functionality is in early access as of Coder v2.21 and is subject to change. -> Do not run in production as it is unstable. -> Instead, deploy these changes into a demo or staging environment. +> This functionality is in early access as of Coder v2.21 and is still evolving. +> For now, we recommend testing it in a demo or staging environment, +> rather than deploying to production > > Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents) > diff --git a/goose/README.md b/goose/README.md index ed5e09ed..59a8560c 100644 --- a/goose/README.md +++ b/goose/README.md @@ -35,9 +35,9 @@ Your workspace must have `screen` installed to use this. ### Run in the background and report tasks (Experimental) -> This functionality is in early access as of Coder v2.21 and is subject to change. -> Do not run in production as it is unstable. -> Instead, deploy these changes into a demo or staging environment. +> This functionality is in early access as of Coder v2.21 and is still evolving. +> For now, we recommend testing it in a demo or staging environment, +> rather than deploying to production > > Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents) > From df98cf4eb9c5f885599df36c48785830e85845b4 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Tue, 8 Apr 2025 18:30:02 -0500 Subject: [PATCH 13/41] chore: fix some typos (#423) --- claude-code/README.md | 2 +- goose/README.md | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/claude-code/README.md b/claude-code/README.md index c599f658..d851d7cf 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -55,7 +55,7 @@ variable "anthropic_api_key" { module "coder-login" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/coder-login/coder" - version = "1.0.31" + version = "1.0.15" agent_id = coder_agent.example.id } diff --git a/goose/README.md b/goose/README.md index 59a8560c..97221e78 100644 --- a/goose/README.md +++ b/goose/README.md @@ -48,7 +48,7 @@ Your workspace must have `screen` installed to use this. module "coder-login" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/coder-login/coder" - version = "1.0.31" + version = "1.0.15" agent_id = coder_agent.example.id } @@ -99,6 +99,9 @@ module "goose" { # Enable experimental features experiment_report_tasks = true + # Run Goose in the background + experiment_use_screen = true + # Avoid configuring Goose manually experiment_auto_configure = true From b3d50ac550e694fad237c700a210017890c4e52f Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Wed, 16 Apr 2025 13:23:25 +0500 Subject: [PATCH 14/41] chore: disable version check in ci (#431) --- .github/workflows/ci.yaml | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2de2364c..6aad7b6c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,16 +51,18 @@ jobs: uses: crate-ci/typos@v1.17.2 - name: Lint run: bun lint - - name: Check version - shell: bash - run: | - # check for version changes - ./update-version.sh - # Check if any changes were made in README.md files - if [[ -n "$(git status --porcelain -- '**/README.md')" ]]; then - echo "Version mismatch detected. Please run ./update-version.sh and commit the updated README.md files." - git diff -- '**/README.md' - exit 1 - else - echo "No version mismatch detected. All versions are up to date." - fi + # Disable version check until https://github.com/coder/modules/pull/426 is merged. + # This will alow us to use seperate versioning for each module without failing CI. The backend already supports that. + # - name: Check version + # shell: bash + # run: | + # # check for version changes + # ./update-version.sh + # # Check if any changes were made in README.md files + # if [[ -n "$(git status --porcelain -- '**/README.md')" ]]; then + # echo "Version mismatch detected. Please run ./update-version.sh and commit the updated README.md files." + # git diff -- '**/README.md' + # exit 1 + # else + # echo "No version mismatch detected. All versions are up to date." + # fi From e175b1a79ea3e0173fcc56208a037f6edc1e56cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 09:04:17 +0000 Subject: [PATCH 15/41] chore: bump crate-ci/typos from 1.17.2 to 1.31.1 (#419) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Muhammad Atif Ali --- .github/dependabot.yml | 2 ++ .github/typos.toml | 2 ++ .github/workflows/ci.yaml | 6 ++++-- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .github/typos.toml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5ace4600..600a98c2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,5 @@ updates: directory: "/" schedule: interval: "weekly" + commit-message: + prefix: "chore" diff --git a/.github/typos.toml b/.github/typos.toml new file mode 100644 index 00000000..ec620a44 --- /dev/null +++ b/.github/typos.toml @@ -0,0 +1,2 @@ +[default.extend-words] +muc = "muc" # For Munich location code diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6aad7b6c..46ed2f88 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,11 +48,13 @@ jobs: - name: Format run: bun fmt:ci - name: typos-action - uses: crate-ci/typos@v1.17.2 + uses: crate-ci/typos@v1.31.1 + with: + config: .github/typos.toml - name: Lint run: bun lint # Disable version check until https://github.com/coder/modules/pull/426 is merged. - # This will alow us to use seperate versioning for each module without failing CI. The backend already supports that. + # This will allow us to use separate versioning for each module without failing CI. The backend already supports that. # - name: Check version # shell: bash # run: | From cccee317365297ff1901549199b93504638722f9 Mon Sep 17 00:00:00 2001 From: Phorcys <57866459+phorcys420@users.noreply.github.com> Date: Wed, 16 Apr 2025 11:39:28 +0200 Subject: [PATCH 16/41] fix: use the proper logs for `filebrowser` and add generic `testBaseLine` function (#418) --- filebrowser/main.test.ts | 105 +++++++++++++++++++++++++++++++++++++++ filebrowser/run.sh | 2 + test.ts | 18 +++++-- 3 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 filebrowser/main.test.ts diff --git a/filebrowser/main.test.ts b/filebrowser/main.test.ts new file mode 100644 index 00000000..34680880 --- /dev/null +++ b/filebrowser/main.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from "bun:test"; +import { + executeScriptInContainer, + runTerraformApply, + runTerraformInit, + type scriptOutput, + testRequiredVariables, +} from "../test"; + +function testBaseLine(output: scriptOutput) { + expect(output.exitCode).toBe(0); + + const expectedLines = [ + "\u001b[[0;1mInstalling filebrowser ", + "đŸĨŗ Installation complete! ", + "👷 Starting filebrowser in background... ", + "📂 Serving /root at http://localhost:13339 ", + "📝 Logs at /tmp/filebrowser.log", + ]; + + // we could use expect(output.stdout).toEqual(expect.arrayContaining(expectedLines)), but when it errors, it doesn't say which line is wrong + for (const line of expectedLines) { + expect(output.stdout).toContain(line); + } +} + +describe("filebrowser", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it("fails with wrong database_path", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + database_path: "nofb", + }).catch((e) => { + if (!e.message.startsWith("\nError: Invalid value for variable")) { + throw e; + } + }); + }); + + it("runs with default", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + + const output = await executeScriptInContainer( + state, + "alpine/curl", + "sh", + "apk add bash", + ); + + testBaseLine(output); + }); + + it("runs with database_path var", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + database_path: ".config/filebrowser.db", + }); + + const output = await await executeScriptInContainer( + state, + "alpine/curl", + "sh", + "apk add bash", + ); + + testBaseLine(output); + }); + + it("runs with folder var", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + folder: "/home/coder/project", + }); + const output = await await executeScriptInContainer( + state, + "alpine/curl", + "sh", + "apk add bash", + ); + }); + + it("runs with subdomain=false", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + agent_name: "main", + subdomain: false, + }); + + const output = await await executeScriptInContainer( + state, + "alpine/curl", + "sh", + "apk add bash", + ); + + testBaseLine(output); + }); +}); diff --git a/filebrowser/run.sh b/filebrowser/run.sh index 84810e4e..62f04edf 100644 --- a/filebrowser/run.sh +++ b/filebrowser/run.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -euo pipefail + BOLD='\033[[0;1m' printf "$${BOLD}Installing filebrowser \n\n" diff --git a/test.ts b/test.ts index 0c48ee99..b1b8aa83 100644 --- a/test.ts +++ b/test.ts @@ -30,6 +30,12 @@ export const runContainer = async ( return containerID.trim(); }; +export interface scriptOutput { + exitCode: number; + stdout: string[]; + stderr: string[]; +} + /** * Finds the only "coder_script" resource in the given state and runs it in a * container. @@ -38,13 +44,15 @@ export const executeScriptInContainer = async ( state: TerraformState, image: string, shell = "sh", -): Promise<{ - exitCode: number; - stdout: string[]; - stderr: string[]; -}> => { + before?: string, +): Promise => { const instance = findResourceInstance(state, "coder_script"); const id = await runContainer(image); + + if (before) { + const respBefore = await execContainer(id, [shell, "-c", before]); + } + const resp = await execContainer(id, [shell, "-c", instance.script]); const stdout = resp.stdout.trim().split("\n"); const stderr = resp.stderr.trim().split("\n"); From d4b4ebd109baa75f6e7bcd49762a4e4d8178e4bb Mon Sep 17 00:00:00 2001 From: Vincent Vielle Date: Wed, 16 Apr 2025 12:40:09 +0200 Subject: [PATCH 17/41] feat(devcontainers-cli): add devcontainers-cli module (#425) Related to [this issue](https://github.com/coder/coder/issues/16432) Create a new module to install [@devcontainers/cli](https://github.com/devcontainers/cli) using npm. --------- Co-authored-by: Mathias Fredriksson Co-authored-by: M Atif Ali --- .icons/devcontainers.svg | 2 + devcontainers-cli/README.md | 22 ++++++ devcontainers-cli/main.test.ts | 130 +++++++++++++++++++++++++++++++++ devcontainers-cli/main.tf | 23 ++++++ devcontainers-cli/run.sh | 37 ++++++++++ 5 files changed, 214 insertions(+) create mode 100644 .icons/devcontainers.svg create mode 100644 devcontainers-cli/README.md create mode 100644 devcontainers-cli/main.test.ts create mode 100644 devcontainers-cli/main.tf create mode 100755 devcontainers-cli/run.sh diff --git a/.icons/devcontainers.svg b/.icons/devcontainers.svg new file mode 100644 index 00000000..fb0443bd --- /dev/null +++ b/.icons/devcontainers.svg @@ -0,0 +1,2 @@ + +Codestin Search App \ No newline at end of file diff --git a/devcontainers-cli/README.md b/devcontainers-cli/README.md new file mode 100644 index 00000000..5ed3aed8 --- /dev/null +++ b/devcontainers-cli/README.md @@ -0,0 +1,22 @@ +--- +display_name: devcontainers-cli +description: devcontainers-cli module provides an easy way to install @devcontainers/cli into a workspace +icon: ../.icons/devcontainers.svg +verified: true +maintainer_github: coder +tags: [devcontainers] +--- + +# devcontainers-cli + +The devcontainers-cli module provides an easy way to install [`@devcontainers/cli`](https://github.com/devcontainers/cli) into a workspace. It can be used within any workspace as it runs only if +@devcontainers/cli is not installed yet. +`npm` is required and should be pre-installed in order for the module to work. + +```tf +module "devcontainers-cli" { + source = "registry.coder.com/modules/devcontainers-cli/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` diff --git a/devcontainers-cli/main.test.ts b/devcontainers-cli/main.test.ts new file mode 100644 index 00000000..8872f181 --- /dev/null +++ b/devcontainers-cli/main.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, it } from "bun:test"; +import { + execContainer, + executeScriptInContainer, + findResourceInstance, + runContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, + type TerraformState, +} from "../test"; + +const executeScriptInContainerWithPackageManager = async ( + state: TerraformState, + image: string, + packageManager: string, + shell = "sh", +): Promise<{ + exitCode: number; + stdout: string[]; + stderr: string[]; +}> => { + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer(image); + + // Install the specified package manager + if (packageManager === "npm") { + await execContainer(id, [shell, "-c", "apk add nodejs npm"]); + } else if (packageManager === "pnpm") { + await execContainer(id, [ + shell, + "-c", + "apk add nodejs npm && npm install -g pnpm", + ]); + } else if (packageManager === "yarn") { + await execContainer(id, [ + shell, + "-c", + "apk add nodejs npm && npm install -g yarn", + ]); + } + + const resp = await execContainer(id, [shell, "-c", instance.script]); + const stdout = resp.stdout.trim().split("\n"); + const stderr = resp.stderr.trim().split("\n"); + return { + exitCode: resp.exitCode, + stdout, + stderr, + }; +}; + +describe("devcontainers-cli", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "some-agent-id", + }); + + it("misses all package managers", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + }); + const output = await executeScriptInContainer(state, "docker:dind"); + expect(output.exitCode).toBe(1); + expect(output.stderr).toEqual([ + "ERROR: No supported package manager (npm, pnpm, yarn) is installed. Please install one first.", + ]); + }, 15000); + + it("installs devcontainers-cli with npm", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + }); + + const output = await executeScriptInContainerWithPackageManager( + state, + "docker:dind", + "npm", + ); + expect(output.exitCode).toBe(0); + + expect(output.stdout[0]).toEqual( + "Installing @devcontainers/cli using npm...", + ); + expect(output.stdout[output.stdout.length - 1]).toEqual( + "đŸĨŗ @devcontainers/cli has been installed into /usr/local/bin/devcontainer!", + ); + }); + + it("installs devcontainers-cli with yarn", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + }); + + const output = await executeScriptInContainerWithPackageManager( + state, + "docker:dind", + "yarn", + ); + expect(output.exitCode).toBe(0); + + expect(output.stdout[0]).toEqual( + "Installing @devcontainers/cli using yarn...", + ); + expect(output.stdout[output.stdout.length - 1]).toEqual( + "đŸĨŗ @devcontainers/cli has been installed into /usr/local/bin/devcontainer!", + ); + }); + + it("displays warning if docker is not installed", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + }); + + const output = await executeScriptInContainerWithPackageManager( + state, + "alpine", + "npm", + ); + expect(output.exitCode).toBe(0); + + expect(output.stdout[0]).toEqual( + "WARNING: Docker was not found but is required to use @devcontainers/cli, please make sure it is available.", + ); + expect(output.stdout[output.stdout.length - 1]).toEqual( + "đŸĨŗ @devcontainers/cli has been installed into /usr/local/bin/devcontainer!", + ); + }); +}); diff --git a/devcontainers-cli/main.tf b/devcontainers-cli/main.tf new file mode 100644 index 00000000..a2aee348 --- /dev/null +++ b/devcontainers-cli/main.tf @@ -0,0 +1,23 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.17" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +resource "coder_script" "devcontainers-cli" { + agent_id = var.agent_id + display_name = "devcontainers-cli" + icon = "/icon/devcontainers.svg" + script = templatefile("${path.module}/run.sh", {}) + run_on_start = true +} diff --git a/devcontainers-cli/run.sh b/devcontainers-cli/run.sh new file mode 100755 index 00000000..dce7a20e --- /dev/null +++ b/devcontainers-cli/run.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env sh + +# If @devcontainers/cli is already installed, we can skip +if command -v devcontainer > /dev/null 2>&1; then + echo "đŸĨŗ @devcontainers/cli is already installed into $(which devcontainer)!" + exit 0 +fi + +# Check if docker is installed +if ! command -v docker > /dev/null 2>&1; then + echo "WARNING: Docker was not found but is required to use @devcontainers/cli, please make sure it is available." +fi + +# Determine the package manager to use: npm, pnpm, or yarn +if command -v pnpm > /dev/null 2>&1; then + PACKAGE_MANAGER="pnpm" +elif command -v yarn > /dev/null 2>&1; then + PACKAGE_MANAGER="yarn" +elif command -v npm > /dev/null 2>&1; then + PACKAGE_MANAGER="npm" +else + echo "ERROR: No supported package manager (npm, pnpm, yarn) is installed. Please install one first." 1>&2 + exit 1 +fi + +echo "Installing @devcontainers/cli using $PACKAGE_MANAGER..." + +# Install @devcontainers/cli using the selected package manager +if [ "$PACKAGE_MANAGER" = "npm" ] || [ "$PACKAGE_MANAGER" = "pnpm" ]; then + $PACKAGE_MANAGER install -g @devcontainers/cli \ + && echo "đŸĨŗ @devcontainers/cli has been installed into $(which devcontainer)!" +elif [ "$PACKAGE_MANAGER" = "yarn" ]; then + $PACKAGE_MANAGER global add @devcontainers/cli \ + && echo "đŸĨŗ @devcontainers/cli has been installed into $(which devcontainer)!" +fi + +exit 0 From 8c2ea35c5a26fa8ba493ea4cf4cc60343d0a50f4 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Fri, 18 Apr 2025 11:16:00 -0500 Subject: [PATCH 18/41] feat: add preinstall/postinstall script + tools for goose and claude code (#424) Co-authored-by: = <=> --- claude-code/main.tf | 33 ++++++++++ goose/README.md | 30 +++++++++ goose/main.tf | 150 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 179 insertions(+), 34 deletions(-) diff --git a/claude-code/main.tf b/claude-code/main.tf index 349af17f..725c1f52 100644 --- a/claude-code/main.tf +++ b/claude-code/main.tf @@ -60,6 +60,23 @@ variable "experiment_report_tasks" { default = false } +variable "experiment_pre_install_script" { + type = string + description = "Custom script to run before installing Claude Code." + default = null +} + +variable "experiment_post_install_script" { + type = string + description = "Custom script to run after installing Claude Code." + default = null +} + +locals { + encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : "" + encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : "" +} + # Install and Initialize Claude Code resource "coder_script" "claude_code" { agent_id = var.agent_id @@ -74,6 +91,14 @@ resource "coder_script" "claude_code" { command -v "$1" >/dev/null 2>&1 } + # Run pre-install script if provided + if [ -n "${local.encoded_pre_install_script}" ]; then + echo "Running pre-install script..." + echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh + chmod +x /tmp/pre_install.sh + /tmp/pre_install.sh + fi + # Install Claude Code if enabled if [ "${var.install_claude_code}" = "true" ]; then if ! command_exists npm; then @@ -84,6 +109,14 @@ resource "coder_script" "claude_code" { npm install -g @anthropic-ai/claude-code@${var.claude_code_version} fi + # Run post-install script if provided + if [ -n "${local.encoded_post_install_script}" ]; then + echo "Running post-install script..." + echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh + chmod +x /tmp/post_install.sh + /tmp/post_install.sh + fi + if [ "${var.experiment_report_tasks}" = "true" ]; then echo "Configuring Claude Code to report tasks via Coder MCP..." coder exp mcp configure claude-code ${var.folder} diff --git a/goose/README.md b/goose/README.md index 97221e78..ff28fcc1 100644 --- a/goose/README.md +++ b/goose/README.md @@ -111,6 +111,36 @@ module "goose" { } ``` +### Adding Custom Extensions (MCP) + +You can extend Goose's capabilities by adding custom extensions. For example, to add the desktop-commander extension: + +```tf +module "goose" { + # ... other configuration ... + + experiment_pre_install_script = <<-EOT + npm i -g @wonderwhy-er/desktop-commander@latest + EOT + + experiment_additional_extensions = <<-EOT + desktop-commander: + args: [] + cmd: desktop-commander + description: Ideal for background tasks + enabled: true + envs: {} + name: desktop-commander + timeout: 300 + type: stdio + EOT +} +``` + +This will add the desktop-commander extension to Goose, allowing it to run commands in the background. The extension will be available in the Goose interface and can be used to run long-running processes like development servers. + +Note: The indentation in the heredoc is preserved, so you can write the YAML naturally. + ## Run standalone Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or any task reporting to the Coder UI. diff --git a/goose/main.tf b/goose/main.tf index fcb6baaa..0043000e 100644 --- a/goose/main.tf +++ b/goose/main.tf @@ -78,6 +78,60 @@ variable "experiment_goose_model" { default = null } +variable "experiment_pre_install_script" { + type = string + description = "Custom script to run before installing Goose." + default = null +} + +variable "experiment_post_install_script" { + type = string + description = "Custom script to run after installing Goose." + default = null +} + +variable "experiment_additional_extensions" { + type = string + description = "Additional extensions configuration in YAML format to append to the config." + default = null +} + +locals { + base_extensions = <<-EOT +coder: + args: + - exp + - mcp + - server + cmd: coder + description: Report ALL tasks and statuses (in progress, done, failed) you are working on. + enabled: true + envs: + CODER_MCP_APP_STATUS_SLUG: goose + name: Coder + timeout: 3000 + type: stdio +developer: + display_name: Developer + enabled: true + name: developer + timeout: 300 + type: builtin +EOT + + # Add two spaces to each line of extensions to match YAML structure + formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}" + additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : "" + + combined_extensions = <<-EOT +extensions: +${local.formatted_base}${local.additional_extensions} +EOT + + encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : "" + encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : "" +} + # Install and Initialize Goose resource "coder_script" "goose" { agent_id = var.agent_id @@ -92,6 +146,14 @@ resource "coder_script" "goose" { command -v "$1" >/dev/null 2>&1 } + # Run pre-install script if provided + if [ -n "${local.encoded_pre_install_script}" ]; then + echo "Running pre-install script..." + echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh + chmod +x /tmp/pre_install.sh + /tmp/pre_install.sh + fi + # Install Goose if enabled if [ "${var.install_goose}" = "true" ]; then if ! command_exists npm; then @@ -102,6 +164,14 @@ resource "coder_script" "goose" { RELEASE_TAG=v${var.goose_version} curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash fi + # Run post-install script if provided + if [ -n "${local.encoded_post_install_script}" ]; then + echo "Running post-install script..." + echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh + chmod +x /tmp/post_install.sh + /tmp/post_install.sh + fi + # Configure Goose if auto-configure is enabled if [ "${var.experiment_auto_configure}" = "true" ]; then echo "Configuring Goose..." @@ -109,29 +179,14 @@ resource "coder_script" "goose" { cat > "$HOME/.config/goose/config.yaml" << EOL GOOSE_PROVIDER: ${var.experiment_goose_provider} GOOSE_MODEL: ${var.experiment_goose_model} -extensions: - coder: - args: - - exp - - mcp - - server - cmd: coder - description: Report ALL tasks and statuses (in progress, done, failed) before and after starting - enabled: true - envs: - CODER_MCP_APP_STATUS_SLUG: goose - name: Coder - timeout: 3000 - type: stdio - developer: - display_name: Developer - enabled: true - name: developer - timeout: 300 - type: builtin +${trimspace(local.combined_extensions)} EOL fi + # Write system prompt to config + mkdir -p "$HOME/.config/goose" + echo "$GOOSE_SYSTEM_PROMPT" > "$HOME/.config/goose/.goosehints" + # Run with screen if enabled if [ "${var.experiment_use_screen}" = "true" ]; then echo "Running Goose in the background..." @@ -162,14 +217,28 @@ EOL export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 - screen -U -dmS goose bash -c ' + # Determine goose command + if command_exists goose; then + GOOSE_CMD=goose + elif [ -f "$HOME/.local/bin/goose" ]; then + GOOSE_CMD="$HOME/.local/bin/goose" + else + echo "Error: Goose is not installed. Please enable install_goose or install it manually." + exit 1 + fi + + screen -U -dmS goose bash -c " cd ${var.folder} - $HOME/.local/bin/goose run --text "$GOOSE_SYSTEM_PROMPT. Your task: $GOOSE_TASK_PROMPT" --interactive | tee -a "$HOME/.goose.log" - exec bash - ' + \"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\" + /bin/bash + " else # Check if goose is installed before running - if ! command_exists $HOME/.local/bin/goose; then + if command_exists goose; then + GOOSE_CMD=goose + elif [ -f "$HOME/.local/bin/goose" ]; then + GOOSE_CMD="$HOME/.local/bin/goose" + else echo "Error: Goose is not installed. Please enable install_goose or install it manually." exit 1 fi @@ -186,21 +255,34 @@ resource "coder_app" "goose" { #!/bin/bash set -e + # Function to check if a command exists + command_exists() { + command -v "$1" >/dev/null 2>&1 + } + + # Determine goose command + if command_exists goose; then + GOOSE_CMD=goose + elif [ -f "$HOME/.local/bin/goose" ]; then + GOOSE_CMD="$HOME/.local/bin/goose" + else + echo "Error: Goose is not installed. Please enable install_goose or install it manually." + exit 1 + fi + if [ "${var.experiment_use_screen}" = "true" ]; then - if screen -list | grep -q "goose"; then - export LANG=en_US.UTF-8 - export LC_ALL=en_US.UTF-8 - echo "Attaching to existing Goose session." | tee -a "$HOME/.goose.log" - screen -xRR goose - else - echo "Starting a new Goose session." | tee -a "$HOME/.goose.log" - screen -S goose bash -c 'export LANG=en_US.UTF-8; export LC_ALL=en_US.UTF-8; $HOME/.local/bin/goose run --text "Always report status and instructions to Coder, before and after your steps" --interactive | tee -a "$HOME/.goose.log"; exec bash' + # Check if session exists first + if ! screen -list | grep -q "goose"; then + echo "Error: No existing Goose session found. Please wait for the script to start it." + exit 1 fi + # Only attach to existing session + screen -xRR goose else cd ${var.folder} export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 - $HOME/.local/bin/goose + "$GOOSE_CMD" run --text "Review goosehints. Your task: $GOOSE_TASK_PROMPT" --interactive fi EOT icon = var.icon From 201acb720e58672a5435d09ba91e0e4e2a299f97 Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:26:08 -0500 Subject: [PATCH 19/41] feat(claude-code): use tmux instead of screen (#432) --- claude-code/README.md | 12 ++++----- claude-code/main.tf | 62 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/claude-code/README.md b/claude-code/README.md index d851d7cf..6590a7fa 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -14,7 +14,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/modules/claude-code/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder" install_claude_code = true @@ -25,7 +25,7 @@ module "claude-code" { ### Prerequisites - Node.js and npm must be installed in your workspace to install Claude Code -- `screen` must be installed in your workspace to run Claude Code in the background +- Either `screen` or `tmux` must be installed in your workspace to run Claude Code in the background - You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces. @@ -43,7 +43,7 @@ The `codercom/oss-dogfood:latest` container image can be used for testing on con > Join our [Discord channel](https://discord.gg/coder) or > [contact us](https://coder.com/contact) to get help or share feedback. -Your workspace must have `screen` installed to use this. +Your workspace must have either `screen` or `tmux` installed to use this. ```tf variable "anthropic_api_key" { @@ -83,14 +83,14 @@ resource "coder_agent" "main" { module "claude-code" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/claude-code/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder" install_claude_code = true claude_code_version = "0.2.57" # Enable experimental features - experiment_use_screen = true + experiment_use_screen = true # Or use experiment_use_tmux = true to use tmux instead experiment_report_tasks = true } ``` @@ -102,7 +102,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude ```tf module "claude-code" { source = "registry.coder.com/modules/claude-code/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder" install_claude_code = true diff --git a/claude-code/main.tf b/claude-code/main.tf index 725c1f52..281b6c35 100644 --- a/claude-code/main.tf +++ b/claude-code/main.tf @@ -54,6 +54,12 @@ variable "experiment_use_screen" { default = false } +variable "experiment_use_tmux" { + type = bool + description = "Whether to use tmux instead of screen for running Claude Code in the background." + default = false +} + variable "experiment_report_tasks" { type = bool description = "Whether to enable task reporting." @@ -122,6 +128,39 @@ resource "coder_script" "claude_code" { coder exp mcp configure claude-code ${var.folder} fi + # Handle terminal multiplexer selection (tmux or screen) + if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then + echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously." + echo "Please set only one of them to true." + exit 1 + fi + + # Run with tmux if enabled + if [ "${var.experiment_use_tmux}" = "true" ]; then + echo "Running Claude Code in the background with tmux..." + + # Check if tmux is installed + if ! command_exists tmux; then + echo "Error: tmux is not installed. Please install tmux manually." + exit 1 + fi + + touch "$HOME/.claude-code.log" + + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + # Create a new tmux session in detached mode + tmux new-session -d -s claude-code -c ${var.folder} "claude" + + # Send the prompt to the tmux session if needed + if [ -n "$CODER_MCP_CLAUDE_TASK_PROMPT" ]; then + tmux send-keys -t claude-code "$CODER_MCP_CLAUDE_TASK_PROMPT" + sleep 5 + tmux send-keys -t claude-code Enter + fi + fi + # Run with screen if enabled if [ "${var.experiment_use_screen}" = "true" ]; then echo "Running Claude Code in the background..." @@ -182,20 +221,27 @@ resource "coder_app" "claude_code" { #!/bin/bash set -e - if [ "${var.experiment_use_screen}" = "true" ]; then + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + if [ "${var.experiment_use_tmux}" = "true" ]; then + if tmux has-session -t claude-code 2>/dev/null; then + echo "Attaching to existing Claude Code tmux session." | tee -a "$HOME/.claude-code.log" + tmux attach-session -t claude-code + else + echo "Starting a new Claude Code tmux session." | tee -a "$HOME/.claude-code.log" + tmux new-session -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions | tee -a \"$HOME/.claude-code.log\"; exec bash" + fi + elif [ "${var.experiment_use_screen}" = "true" ]; then if screen -list | grep -q "claude-code"; then - export LANG=en_US.UTF-8 - export LC_ALL=en_US.UTF-8 - echo "Attaching to existing Claude Code session." | tee -a "$HOME/.claude-code.log" + echo "Attaching to existing Claude Code screen session." | tee -a "$HOME/.claude-code.log" screen -xRR claude-code else - echo "Starting a new Claude Code session." | tee -a "$HOME/.claude-code.log" - screen -S claude-code bash -c 'export LANG=en_US.UTF-8; export LC_ALL=en_US.UTF-8; claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash' + echo "Starting a new Claude Code screen session." | tee -a "$HOME/.claude-code.log" + screen -S claude-code bash -c 'claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash' fi else cd ${var.folder} - export LANG=en_US.UTF-8 - export LC_ALL=en_US.UTF-8 claude fi EOT From 9d6dd83f6fc93aff228e9e7a160732a926e85932 Mon Sep 17 00:00:00 2001 From: Vincent Vielle Date: Fri, 18 Apr 2025 19:06:10 +0200 Subject: [PATCH 20/41] fix(devcontainers-cli): add PNPM_HOME for pnpm package manager (#433) PNPM requires `$PNPM_HOME` to be set to install packages. This PR is a follow-up on the one creating @devcontainers-cli module - to ensure PNPM_HOME is set, based on values set by Coder. --- devcontainers-cli/README.md | 2 +- devcontainers-cli/main.test.ts | 8 +++---- devcontainers-cli/run.sh | 41 +++++++++++++++++++++++++--------- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/devcontainers-cli/README.md b/devcontainers-cli/README.md index 5ed3aed8..11961cd7 100644 --- a/devcontainers-cli/README.md +++ b/devcontainers-cli/README.md @@ -16,7 +16,7 @@ The devcontainers-cli module provides an easy way to install [`@devcontainers/cl ```tf module "devcontainers-cli" { source = "registry.coder.com/modules/devcontainers-cli/coder" - version = "1.0.0" + version = "1.0.1" agent_id = coder_agent.example.id } ``` diff --git a/devcontainers-cli/main.test.ts b/devcontainers-cli/main.test.ts index 8872f181..85686c46 100644 --- a/devcontainers-cli/main.test.ts +++ b/devcontainers-cli/main.test.ts @@ -30,7 +30,7 @@ const executeScriptInContainerWithPackageManager = async ( await execContainer(id, [ shell, "-c", - "apk add nodejs npm && npm install -g pnpm", + `wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.shrc" SHELL="$(which sh)" sh -`, ]); } else if (packageManager === "yarn") { await execContainer(id, [ @@ -86,7 +86,7 @@ describe("devcontainers-cli", async () => { expect(output.stdout[output.stdout.length - 1]).toEqual( "đŸĨŗ @devcontainers/cli has been installed into /usr/local/bin/devcontainer!", ); - }); + }, 15000); it("installs devcontainers-cli with yarn", async () => { const state = await runTerraformApply(import.meta.dir, { @@ -106,7 +106,7 @@ describe("devcontainers-cli", async () => { expect(output.stdout[output.stdout.length - 1]).toEqual( "đŸĨŗ @devcontainers/cli has been installed into /usr/local/bin/devcontainer!", ); - }); + }, 15000); it("displays warning if docker is not installed", async () => { const state = await runTerraformApply(import.meta.dir, { @@ -126,5 +126,5 @@ describe("devcontainers-cli", async () => { expect(output.stdout[output.stdout.length - 1]).toEqual( "đŸĨŗ @devcontainers/cli has been installed into /usr/local/bin/devcontainer!", ); - }); + }, 15000); }); diff --git a/devcontainers-cli/run.sh b/devcontainers-cli/run.sh index dce7a20e..03aac17f 100755 --- a/devcontainers-cli/run.sh +++ b/devcontainers-cli/run.sh @@ -12,26 +12,45 @@ if ! command -v docker > /dev/null 2>&1; then fi # Determine the package manager to use: npm, pnpm, or yarn -if command -v pnpm > /dev/null 2>&1; then - PACKAGE_MANAGER="pnpm" -elif command -v yarn > /dev/null 2>&1; then +if command -v yarn > /dev/null 2>&1; then PACKAGE_MANAGER="yarn" elif command -v npm > /dev/null 2>&1; then PACKAGE_MANAGER="npm" +elif command -v pnpm > /dev/null 2>&1; then + PACKAGE_MANAGER="pnpm" else echo "ERROR: No supported package manager (npm, pnpm, yarn) is installed. Please install one first." 1>&2 exit 1 fi -echo "Installing @devcontainers/cli using $PACKAGE_MANAGER..." +install() { + echo "Installing @devcontainers/cli using $PACKAGE_MANAGER..." + if [ "$PACKAGE_MANAGER" = "npm" ]; then + npm install -g @devcontainers/cli + elif [ "$PACKAGE_MANAGER" = "pnpm" ]; then + # Check if PNPM_HOME is set, if not, set it to the script's bin directory + # pnpm needs this to be set to install binaries + # coder agent ensures this part is part of the PATH + # so that the devcontainer command is available + if [ -z "$PNPM_HOME" ]; then + PNPM_HOME="$CODER_SCRIPT_BIN_DIR" + export M_HOME + fi + pnpm add -g @devcontainers/cli + elif [ "$PACKAGE_MANAGER" = "yarn" ]; then + yarn global add @devcontainers/cli + fi +} + +if ! install; then + echo "Failed to install @devcontainers/cli" >&2 + exit 1 +fi -# Install @devcontainers/cli using the selected package manager -if [ "$PACKAGE_MANAGER" = "npm" ] || [ "$PACKAGE_MANAGER" = "pnpm" ]; then - $PACKAGE_MANAGER install -g @devcontainers/cli \ - && echo "đŸĨŗ @devcontainers/cli has been installed into $(which devcontainer)!" -elif [ "$PACKAGE_MANAGER" = "yarn" ]; then - $PACKAGE_MANAGER global add @devcontainers/cli \ - && echo "đŸĨŗ @devcontainers/cli has been installed into $(which devcontainer)!" +if ! command -v devcontainer > /dev/null 2>&1; then + echo "Installation completed but 'devcontainer' command not found in PATH" >&2 + exit 1 fi +echo "đŸĨŗ @devcontainers/cli has been installed into $(which devcontainer)!" exit 0 From fd2dec79023ebfe439e5db4f039ac08d62ade90d Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko <2305836+valeneiko@users.noreply.github.com> Date: Sun, 20 Apr 2025 11:48:36 +0100 Subject: [PATCH 21/41] feat(code-server): make `open_in` configurable (#430) Co-authored-by: M Atif Ali --- code-server/README.md | 14 +++++++------- code-server/main.tf | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/code-server/README.md b/code-server/README.md index e7098113..dc44237f 100644 --- a/code-server/README.md +++ b/code-server/README.md @@ -15,7 +15,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id } ``` @@ -30,7 +30,7 @@ module "code-server" { module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id install_version = "4.8.3" } @@ -44,7 +44,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/): module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id extensions = [ "dracula-theme.theme-dracula" @@ -62,7 +62,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -79,7 +79,7 @@ Just run code-server in the background, don't fetch it from GitHub: module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] } @@ -95,7 +95,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub: module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id use_cached = true extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] @@ -108,7 +108,7 @@ Just run code-server in the background, don't fetch it from GitHub: module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/code-server/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id offline = true } diff --git a/code-server/main.tf b/code-server/main.tf index c80e5378..ca4ff3af 100644 --- a/code-server/main.tf +++ b/code-server/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 0.17" + version = ">= 2.1" } } } @@ -122,6 +122,20 @@ variable "subdomain" { default = false } +variable "open_in" { + type = string + description = <<-EOT + Determines where the app will be opened. Valid values are `"tab"` and `"slim-window" (default)`. + `"tab"` opens in a new tab in the same browser window. + `"slim-window"` opens a new browser window without navigation controls. + EOT + default = "slim-window" + validation { + condition = contains(["tab", "slim-window"], var.open_in) + error_message = "The 'open_in' variable must be one of: 'tab', 'slim-window'." + } +} + resource "coder_script" "code-server" { agent_id = var.agent_id display_name = "code-server" @@ -166,6 +180,7 @@ resource "coder_app" "code-server" { subdomain = var.subdomain share = var.share order = var.order + open_in = var.open_in healthcheck { url = "http://localhost:${var.port}/healthz" From 5c5cf8ecb4ae50a2c5e0db63a3b3a1409d6adad6 Mon Sep 17 00:00:00 2001 From: Birdie Kingston Date: Tue, 22 Apr 2025 19:51:46 +1000 Subject: [PATCH 22/41] fix(vault-jwt): store vault token for use in vault jwt module (#435) Co-authored-by: Birdie K <5210502+moo-im-a-cow@users.noreply.github.com> --- vault-jwt/README.md | 8 ++++---- vault-jwt/run.sh | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vault-jwt/README.md b/vault-jwt/README.md index 66070397..6da29124 100644 --- a/vault-jwt/README.md +++ b/vault-jwt/README.md @@ -16,7 +16,7 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec module "vault" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vault-jwt/coder" - version = "1.0.20" + version = "1.0.21" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_jwt_role = "coder" # The Vault role to use for authentication @@ -43,7 +43,7 @@ curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/d module "vault" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vault-jwt/coder" - version = "1.0.20" + version = "1.0.21" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_jwt_auth_path = "oidc" @@ -59,7 +59,7 @@ data "coder_workspace_owner" "me" {} module "vault" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vault-jwt/coder" - version = "1.0.20" + version = "1.0.21" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_jwt_role = data.coder_workspace_owner.me.groups[0] @@ -72,7 +72,7 @@ module "vault" { module "vault" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vault-jwt/coder" - version = "1.0.20" + version = "1.0.21" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_jwt_role = "coder" # The Vault role to use for authentication diff --git a/vault-jwt/run.sh b/vault-jwt/run.sh index ef45884d..d95b45a2 100644 --- a/vault-jwt/run.sh +++ b/vault-jwt/run.sh @@ -107,6 +107,6 @@ rm -rf "$TMP" # Authenticate with Vault printf "🔑 Authenticating with Vault ...\n\n" -echo "$${CODER_OIDC_ACCESS_TOKEN}" | vault write auth/"$${VAULT_JWT_AUTH_PATH}"/login role="$${VAULT_JWT_ROLE}" jwt=- +echo "$${CODER_OIDC_ACCESS_TOKEN}" | vault write -field=token auth/"$${VAULT_JWT_AUTH_PATH}"/login role="$${VAULT_JWT_ROLE}" jwt=- | vault login - printf "đŸĨŗ Vault authentication complete!\n\n" printf "You can now use Vault CLI to access secrets.\n" From 856400c53d828b9ae63df7cb908e5d7917a9df99 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Tue, 22 Apr 2025 17:31:51 +0500 Subject: [PATCH 23/41] chore: add support for separate module versioning to CI (#426) Co-authored-by: Mathias Fredriksson --- .github/workflows/ci.yaml | 15 --- CONTRIBUTING.md | 50 +++++++--- package.json | 5 +- release.sh | 196 ++++++++++++++++++++++++++++++++++++++ update-version.sh | 65 ------------- update_version.sh | 146 ++++++++++++++++++++++++++++ 6 files changed, 380 insertions(+), 97 deletions(-) create mode 100755 release.sh delete mode 100755 update-version.sh create mode 100755 update_version.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 46ed2f88..f0c2425a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,18 +53,3 @@ jobs: config: .github/typos.toml - name: Lint run: bun lint - # Disable version check until https://github.com/coder/modules/pull/426 is merged. - # This will allow us to use separate versioning for each module without failing CI. The backend already supports that. - # - name: Check version - # shell: bash - # run: | - # # check for version changes - # ./update-version.sh - # # Check if any changes were made in README.md files - # if [[ -n "$(git status --porcelain -- '**/README.md')" ]]; then - # echo "Version mismatch detected. Please run ./update-version.sh and commit the updated README.md files." - # git diff -- '**/README.md' - # exit 1 - # else - # echo "No version mismatch detected. All versions are up to date." - # fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60b1260b..2c7ba8bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,8 @@ Follow the instructions to ensure that Bun is available globally. Once Bun has b ## Testing a Module -> **Note:** It is the responsibility of the module author to implement tests for their module. The author must test the module locally before submitting a PR. +> [!NOTE] +> It is the responsibility of the module author to implement tests for their module. The author must test the module locally before submitting a PR. A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers. @@ -53,23 +54,44 @@ module "example" { ## Releases -> [!WARNING] -> When creating a new release, make sure that your new version number is fully accurate. If a version number is incorrect or does not exist, we may end up serving incorrect/old data for our various tools and providers. +The release process is automated with these steps: + +## 1. Create and Merge PR + +- Create a PR with your module changes +- Get your PR reviewed, approved, and merged to `main` + +## 2. Prepare Release (Maintainer Task) + +After merging to `main`, a maintainer will: + +- View all modules and their current versions: + + ```shell + ./release.sh --list + ``` + +- Determine the next version number based on changes: + + - **Patch version** (1.2.3 → 1.2.4): Bug fixes + - **Minor version** (1.2.3 → 1.3.0): New features, adding inputs, deprecating inputs + - **Major version** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing input types) -Much of our release process is automated. To cut a new release: +- Create and push an annotated tag: -1. Navigate to [GitHub's Releases page](https://github.com/coder/modules/releases) -2. Click "Draft a new release" -3. Click the "Choose a tag" button and type a new release number in the format `v..` (e.g., `v1.18.0`). Then click "Create new tag". -4. Click the "Generate release notes" button, and clean up the resulting README. Be sure to remove any notes that would not be relevant to end-users (e.g., bumping dependencies). -5. Once everything looks good, click the "Publish release" button. + ```shell + # Fetch latest changes + git fetch origin + + # Create and push tag + ./release.sh module-name 1.2.3 --push + ``` -Once the release has been cut, a script will run to check whether there are any modules that will require that the new release number be published to Terraform. If there are any, a new pull request will automatically be generated. Be sure to approve this PR and merge it into the `main` branch. + The tag format will be: `release/module-name/v1.2.3` -Following that, our automated processes will handle publishing new data for [`registry.coder.com`](https://github.com/coder/registry.coder.com/): +## 3. Publishing to Registry -1. Publishing new versions to Coder's [Terraform Registry](https://registry.terraform.io/providers/coder/coder/latest) -2. Publishing new data to the [Coder Registry](https://registry.coder.com) +Our automated processes will handle publishing new data to [registry.coder.com](https://registry.coder.com). > [!NOTE] -> Some data in `registry.coder.com` is fetched on demand from the Module repo's main branch. This data should be updated almost immediately after a new release, but other changes will take some time to propagate. +> Some data in registry.coder.com is fetched on demand from the [coder/modules](https://github.com/coder/modules) repo's `main` branch. This data should update almost immediately after a release, while other changes will take some time to propagate. diff --git a/package.json b/package.json index eea421d8..a122f4f2 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,9 @@ "name": "modules", "scripts": { "test": "bun test", - "fmt": "bun x prettier -w **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt **/*.tf .sample/main.tf", + "fmt": "bun x prettier -w **/*.sh .sample/run.sh new.sh terraform_validate.sh release.sh update_version.sh **/*.ts **/*.md *.md && terraform fmt **/*.tf .sample/main.tf", "fmt:ci": "bun x prettier --check **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt -check **/*.tf .sample/main.tf", - "lint": "bun run lint.ts && ./terraform_validate.sh", - "update-version": "./update-version.sh" + "lint": "bun run lint.ts && ./terraform_validate.sh" }, "devDependencies": { "bun-types": "^1.1.23", diff --git a/release.sh b/release.sh new file mode 100755 index 00000000..167fc1cd --- /dev/null +++ b/release.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat < ] + +Create annotated git tags for module releases. + +This script is used by maintainers to create annotated tags for module +releases. When a tag is pushed, it triggers a GitHub workflow that +updates README versions. + +Options: + -l, --list List all modules with their versions + -n, --dry-run Show what would be done without making changes + -p, --push Push the created tag to the remote repository + -h, --help Show this help message + +Examples: + $0 --list + $0 nodejs 1.2.3 + $0 nodejs 1.2.3 --push + $0 --dry-run nodejs 1.2.3 +EOF + exit "${1:-0}" +} + +check_getopt() { + # Check if we have GNU or BSD getopt. + if getopt --test >/dev/null 2>&1; then + # Exit status 4 means GNU getopt is available. + if [[ $? -ne 4 ]]; then + echo "Error: GNU getopt is not available." >&2 + echo "On macOS, you can install GNU getopt and add it to your PATH:" >&2 + echo + echo $'\tbrew install gnu-getopt' >&2 + echo $'\texport PATH="$(brew --prefix gnu-getopt)/bin:$PATH"' >&2 + exit 1 + fi + fi +} + +maybe_dry_run() { + if [[ $dry_run == true ]]; then + echo "[DRY RUN] $*" + return 0 + fi + "$@" +} + +get_readme_version() { + grep -o 'version *= *"[0-9]\+\.[0-9]\+\.[0-9]\+"' "$1" | + head -1 | + grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || + echo "0.0.0" +} + +list_modules() { + printf "\nListing all modules and their latest versions:\n" + printf "%s\n" "--------------------------------------------------------------" + printf "%-30s %-15s %-15s\n" "MODULE" "README VERSION" "LATEST TAG" + printf "%s\n" "--------------------------------------------------------------" + + # Process each module directory. + for dir in */; do + # Skip non-module directories. + [[ ! -d $dir || ! -f ${dir}README.md || $dir == ".git/" ]] && continue + + module="${dir%/}" + readme_version=$(get_readme_version "${dir}README.md") + latest_tag=$(git tag -l "release/${module}/v*" | sort -V | tail -n 1) + tag_version="none" + if [[ -n $latest_tag ]]; then + tag_version="${latest_tag#"release/${module}/v"}" + fi + + printf "%-30s %-15s %-15s\n" "$module" "$readme_version" "$tag_version" + done + + printf "%s\n" "--------------------------------------------------------------" +} + +is_valid_version() { + if ! [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must be in format X.Y.Z (e.g., 1.2.3)" >&2 + return 1 + fi +} + +get_tag_name() { + local module="$1" + local version="$2" + local tag_name="release/$module/v$version" + local readme_path="$module/README.md" + + if [[ ! -d $module || ! -f $readme_path ]]; then + echo "Error: Module '$module' not found or missing README.md" >&2 + return 1 + fi + + local readme_version + readme_version=$(get_readme_version "$readme_path") + + { + echo "Module: $module" + echo "Current README version: $readme_version" + echo "New tag version: $version" + echo "Tag name: $tag_name" + } >&2 + + echo "$tag_name" +} + +# Ensure getopt is available. +check_getopt + +# Set defaults. +list=false +dry_run=false +push=false +module= +version= + +# Parse command-line options. +if ! temp=$(getopt -o ldph --long list,dry-run,push,help -n "$0" -- "$@"); then + echo "Error: Failed to parse arguments" >&2 + usage 1 +fi +eval set -- "$temp" + +while true; do + case "$1" in + -l | --list) + list=true + shift + ;; + -d | --dry-run) + dry_run=true + shift + ;; + -p | --push) + push=true + shift + ;; + -h | --help) + usage + ;; + --) + shift + break + ;; + *) + echo "Error: Internal error!" >&2 + exit 1 + ;; + esac +done + +if [[ $list == true ]]; then + list_modules + exit 0 +fi + +if [[ $# -ne 2 ]]; then + echo "Error: MODULE and VERSION are required when not using --list" >&2 + usage 1 +fi + +module="$1" +version="$2" + +if ! is_valid_version "$version"; then + exit 1 +fi + +if ! tag_name=$(get_tag_name "$module" "$version"); then + exit 1 +fi + +if git rev-parse -q --verify "refs/tags/$tag_name" >/dev/null 2>&1; then + echo "Notice: Tag '$tag_name' already exists" >&2 +else + maybe_dry_run git tag -a "$tag_name" -m "Release $module v$version" + if [[ $push == true ]]; then + maybe_dry_run echo "Tag '$tag_name' created." + else + maybe_dry_run echo "Tag '$tag_name' created locally. Use --push to push it to remote." + maybe_dry_run "â„šī¸ Note: Remember to push the tag when ready." + fi +fi + +if [[ $push == true ]]; then + maybe_dry_run git push origin "$tag_name" + maybe_dry_run echo "Success! Tag '$tag_name' pushed to remote." +fi diff --git a/update-version.sh b/update-version.sh deleted file mode 100755 index 09547f9c..00000000 --- a/update-version.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash - -# This script increments the version number in the README.md files of all modules -# by 1 patch version. It is intended to be run from the root -# of the repository or by using the `bun update-version` command. - -set -euo pipefail - -current_tag=$(git describe --tags --abbrev=0) - -# Increment the patch version -LATEST_TAG=$(echo "$current_tag" | sed 's/^v//' | awk -F. '{print $1"."$2"."$3+1}') || exit $? - -# List directories with changes that are not README.md or test files -mapfile -t changed_dirs < <(git diff --name-only "$current_tag" -- ':!**/README.md' ':!**/*.test.ts' | xargs dirname | grep -v '^\.' | sort -u) - -echo "Directories with changes: ${changed_dirs[*]}" - -# Iterate over directories and update version in README.md -for dir in "${changed_dirs[@]}"; do - if [[ -f "$dir/README.md" ]]; then - file="$dir/README.md" - tmpfile=$(mktemp /tmp/tempfile.XXXXXX) - awk -v tag="$LATEST_TAG" ' - BEGIN { in_code_block = 0; in_nested_block = 0 } - { - # Detect the start and end of Markdown code blocks. - if ($0 ~ /^```/) { - in_code_block = !in_code_block - # Reset nested block tracking when exiting a code block. - if (!in_code_block) { - in_nested_block = 0 - } - } - - # Handle nested blocks within a code block. - if (in_code_block) { - # Detect the start of a nested block (skipping "module" blocks). - if ($0 ~ /{/ && !($1 == "module" || $1 ~ /^[a-zA-Z0-9_]+$/)) { - in_nested_block++ - } - - # Detect the end of a nested block. - if ($0 ~ /}/ && in_nested_block > 0) { - in_nested_block-- - } - - # Update "version" only if not in a nested block. - if (!in_nested_block && $1 == "version" && $2 == "=") { - sub(/"[^"]*"/, "\"" tag "\"") - } - } - - print - } - ' "$file" > "$tmpfile" && mv "$tmpfile" "$file" - - # Check if the README.md file has changed - if ! git diff --quiet -- "$dir/README.md"; then - echo "Bumping version in $dir/README.md from $current_tag to $LATEST_TAG (incremented)" - else - echo "Version in $dir/README.md is already up to date" - fi - fi -done diff --git a/update_version.sh b/update_version.sh new file mode 100755 index 00000000..1f85a907 --- /dev/null +++ b/update_version.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat < + +Update or check the version in a module's README.md file. + +Options: + -c, --check Check if README.md version matches VERSION without updating + -h, --help Display this help message and exit + +Examples: + $0 code-server 1.2.3 # Update version in code-server/README.md + $0 --check code-server 1.2.3 # Check if version matches 1.2.3 +EOF + exit "${1:-0}" +} + +is_valid_version() { + if ! [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must be in format X.Y.Z (e.g., 1.2.3)" >&2 + return 1 + fi +} + +update_version() { + local file="$1" current_tag="$2" latest_tag="$3" tmpfile + tmpfile=$(mktemp) + + echo "Updating version in $file from $current_tag to $latest_tag..." + + awk -v tag="$latest_tag" ' + BEGIN { in_code_block = 0; in_nested_block = 0 } + { + # Detect the start and end of Markdown code blocks. + if ($0 ~ /^```/) { + in_code_block = !in_code_block + # Reset nested block tracking when exiting a code block. + if (!in_code_block) { + in_nested_block = 0 + } + } + + # Handle nested blocks within a code block. + if (in_code_block) { + # Detect the start of a nested block (skipping "module" blocks). + if ($0 ~ /{/ && !($1 == "module" || $1 ~ /^[a-zA-Z0-9_]+$/)) { + in_nested_block++ + } + + # Detect the end of a nested block. + if ($0 ~ /}/ && in_nested_block > 0) { + in_nested_block-- + } + + # Update "version" only if not in a nested block. + if (!in_nested_block && $1 == "version" && $2 == "=") { + sub(/"[^"]*"/, "\"" tag "\"") + } + } + + print + } + ' "$file" >"$tmpfile" && mv "$tmpfile" "$file" +} + +get_readme_version() { + grep -o 'version *= *"[0-9]\+\.[0-9]\+\.[0-9]\+"' "$1" | + head -1 | + grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || + echo "0.0.0" +} + +# Set defaults. +check_only=false + +# Parse command-line options. +while [[ $# -gt 0 ]]; do + case "$1" in + -c | --check) + check_only=true + shift + ;; + -h | --help) + usage 0 + ;; + -*) + echo "Error: Unknown option: $1" >&2 + usage 1 + ;; + *) + break + ;; + esac +done + +if [[ $# -ne 2 ]]; then + echo "Error: MODULE and VERSION are required" >&2 + usage 1 +fi + +module_name="$1" +version="$2" + +if [[ ! -d $module_name ]]; then + echo "Error: Module directory '$module_name' not found" >&2 + echo >&2 + echo "Available modules:" >&2 + echo >&2 + find . -type d -mindepth 1 -maxdepth 1 -not -path "*/\.*" | sed 's|^./|\t|' | sort >&2 + exit 1 +fi + +if ! is_valid_version "$version"; then + exit 1 +fi + +readme_path="$module_name/README.md" +if [[ ! -f $readme_path ]]; then + echo "Error: README.md not found in '$module_name' directory" >&2 + exit 1 +fi + +readme_version=$(get_readme_version "$readme_path") + +# In check mode, just return success/failure based on version match. +if [[ $check_only == true ]]; then + if [[ $readme_version == "$version" ]]; then + echo "✅ Success: Version in $readme_path matches $version" + exit 0 + else + echo "❌ Error: Version mismatch in $readme_path" + echo "Expected: $version" + echo "Found: $readme_version" + exit 1 + fi +fi + +if [[ $readme_version != "$version" ]]; then + update_version "$readme_path" "$readme_version" "$version" + echo "✅ Version updated successfully to $version" +else + echo "â„šī¸ Version in $readme_path already set to $version, no update needed" +fi From 3dc2c9f371e51b8e660bf87101f86621b0f495d0 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Tue, 22 Apr 2025 21:27:32 +0500 Subject: [PATCH 24/41] chore(claude-code): update module version to 1.2.0 (#438) This was missed in #424 --- claude-code/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/claude-code/README.md b/claude-code/README.md index 6590a7fa..e10281bb 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -14,7 +14,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/modules/claude-code/coder" - version = "1.1.0" + version = "1.2.0" agent_id = coder_agent.example.id folder = "/home/coder" install_claude_code = true @@ -71,7 +71,7 @@ data "coder_parameter" "ai_prompt" { resource "coder_agent" "main" { # ... env = { - CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter + CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter CODER_MCP_CLAUDE_TASK_PROMPT = data.coder_parameter.ai_prompt.value CODER_MCP_APP_STATUS_SLUG = "claude-code" CODER_MCP_CLAUDE_SYSTEM_PROMPT = <<-EOT @@ -102,7 +102,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude ```tf module "claude-code" { source = "registry.coder.com/modules/claude-code/coder" - version = "1.1.0" + version = "1.2.0" agent_id = coder_agent.example.id folder = "/home/coder" install_claude_code = true From 6096a4019720cb68b55a97155ce876da6031e4f9 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Tue, 22 Apr 2025 21:28:39 +0500 Subject: [PATCH 25/41] chore(goose): update module version to 1.1.0 (#439) This was missed in #424 --- goose/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/goose/README.md b/goose/README.md index ff28fcc1..89014891 100644 --- a/goose/README.md +++ b/goose/README.md @@ -14,7 +14,7 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener ```tf module "goose" { source = "registry.coder.com/modules/goose/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder" install_goose = true @@ -72,11 +72,11 @@ resource "coder_agent" "main" { env = { GOOSE_SYSTEM_PROMPT = <<-EOT You are a helpful assistant that can help write code. - + Run all long running tasks (e.g. npm run dev) in the background and not in the foreground. - + Periodically check in on background tasks. - + Notify Coder of the status of the task before and after your steps. EOT GOOSE_TASK_PROMPT = data.coder_parameter.ai_prompt.value @@ -90,7 +90,7 @@ resource "coder_agent" "main" { module "goose" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/goose/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder" install_goose = true @@ -148,7 +148,7 @@ Run Goose as a standalone app in your workspace. This will install Goose and run ```tf module "goose" { source = "registry.coder.com/modules/goose/coder" - version = "1.0.31" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder" install_goose = true From 72d3c92d514f235c8aef3ca04181ecc6a2a186b9 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Tue, 22 Apr 2025 21:50:13 +0500 Subject: [PATCH 26/41] chore: format scripts (#440) --- release.sh | 60 +++++++++++++++++++++++------------------------ update_version.sh | 40 +++++++++++++++---------------- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/release.sh b/release.sh index 167fc1cd..b9163918 100755 --- a/release.sh +++ b/release.sh @@ -2,7 +2,7 @@ set -euo pipefail usage() { - cat < ] Create annotated git tags for module releases. @@ -28,7 +28,7 @@ EOF check_getopt() { # Check if we have GNU or BSD getopt. - if getopt --test >/dev/null 2>&1; then + if getopt --test > /dev/null 2>&1; then # Exit status 4 means GNU getopt is available. if [[ $? -ne 4 ]]; then echo "Error: GNU getopt is not available." >&2 @@ -50,10 +50,10 @@ maybe_dry_run() { } get_readme_version() { - grep -o 'version *= *"[0-9]\+\.[0-9]\+\.[0-9]\+"' "$1" | - head -1 | - grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || - echo "0.0.0" + grep -o 'version *= *"[0-9]\+\.[0-9]\+\.[0-9]\+"' "$1" \ + | head -1 \ + | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' \ + || echo "0.0.0" } list_modules() { @@ -131,29 +131,29 @@ eval set -- "$temp" while true; do case "$1" in - -l | --list) - list=true - shift - ;; - -d | --dry-run) - dry_run=true - shift - ;; - -p | --push) - push=true - shift - ;; - -h | --help) - usage - ;; - --) - shift - break - ;; - *) - echo "Error: Internal error!" >&2 - exit 1 - ;; + -l | --list) + list=true + shift + ;; + -d | --dry-run) + dry_run=true + shift + ;; + -p | --push) + push=true + shift + ;; + -h | --help) + usage + ;; + --) + shift + break + ;; + *) + echo "Error: Internal error!" >&2 + exit 1 + ;; esac done @@ -178,7 +178,7 @@ if ! tag_name=$(get_tag_name "$module" "$version"); then exit 1 fi -if git rev-parse -q --verify "refs/tags/$tag_name" >/dev/null 2>&1; then +if git rev-parse -q --verify "refs/tags/$tag_name" > /dev/null 2>&1; then echo "Notice: Tag '$tag_name' already exists" >&2 else maybe_dry_run git tag -a "$tag_name" -m "Release $module v$version" diff --git a/update_version.sh b/update_version.sh index 1f85a907..39430cdd 100755 --- a/update_version.sh +++ b/update_version.sh @@ -2,7 +2,7 @@ set -euo pipefail usage() { - cat < Update or check the version in a module's README.md file. @@ -63,14 +63,14 @@ update_version() { print } - ' "$file" >"$tmpfile" && mv "$tmpfile" "$file" + ' "$file" > "$tmpfile" && mv "$tmpfile" "$file" } get_readme_version() { - grep -o 'version *= *"[0-9]\+\.[0-9]\+\.[0-9]\+"' "$1" | - head -1 | - grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || - echo "0.0.0" + grep -o 'version *= *"[0-9]\+\.[0-9]\+\.[0-9]\+"' "$1" \ + | head -1 \ + | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' \ + || echo "0.0.0" } # Set defaults. @@ -79,20 +79,20 @@ check_only=false # Parse command-line options. while [[ $# -gt 0 ]]; do case "$1" in - -c | --check) - check_only=true - shift - ;; - -h | --help) - usage 0 - ;; - -*) - echo "Error: Unknown option: $1" >&2 - usage 1 - ;; - *) - break - ;; + -c | --check) + check_only=true + shift + ;; + -h | --help) + usage 0 + ;; + -*) + echo "Error: Unknown option: $1" >&2 + usage 1 + ;; + *) + break + ;; esac done From 4b4cebaf4dfbf200ff9cb2a0dc6ff9bbf3101ce8 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Tue, 22 Apr 2025 22:52:53 +0500 Subject: [PATCH 27/41] ci: deploy dev registry on tag pushes (#441) --- .github/workflows/deploy-registry.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-registry.yaml b/.github/workflows/deploy-registry.yaml index 7a67cc83..bbecad96 100644 --- a/.github/workflows/deploy-registry.yaml +++ b/.github/workflows/deploy-registry.yaml @@ -4,6 +4,8 @@ on: push: branches: - main + tags: + - "release/*/v*" # Matches tags like release/module-name/v1.0.0 jobs: deploy: @@ -34,4 +36,4 @@ jobs: - name: Deploy to dev.registry.coder.com run: | gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch dev - \ No newline at end of file + From b58ad58f613ec9dd972a1a3575e4f652940c1104 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Thu, 24 Apr 2025 01:54:51 +0500 Subject: [PATCH 28/41] feat(windsurf): add Windsurf Editor module (#446) --- .icons/windsurf.svg | 43 +++++++++++++++++++++ windsurf/README.md | 37 ++++++++++++++++++ windsurf/main.test.ts | 88 +++++++++++++++++++++++++++++++++++++++++++ windsurf/main.tf | 62 ++++++++++++++++++++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 .icons/windsurf.svg create mode 100644 windsurf/README.md create mode 100644 windsurf/main.test.ts create mode 100644 windsurf/main.tf diff --git a/.icons/windsurf.svg b/.icons/windsurf.svg new file mode 100644 index 00000000..a7684d4c --- /dev/null +++ b/.icons/windsurf.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/windsurf/README.md b/windsurf/README.md new file mode 100644 index 00000000..93f25ebb --- /dev/null +++ b/windsurf/README.md @@ -0,0 +1,37 @@ +--- +display_name: Windsurf Editor +description: Add a one-click button to launch Windsurf Editor +icon: ../.icons/windsurf.svg +maintainer_github: coder +verified: true +tags: [ide, windsurf, helper, ai] +--- + +# Windsurf Editor + +Add a button to open any workspace with a single click in Windsurf Editor. + +Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder). + +```tf +module "windsurf" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/windsurf/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +## Examples + +### Open in a specific directory + +```tf +module "windsurf" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/windsurf/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + folder = "/home/coder/project" +} +``` diff --git a/windsurf/main.test.ts b/windsurf/main.test.ts new file mode 100644 index 00000000..a158962a --- /dev/null +++ b/windsurf/main.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +describe("windsurf", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it("default output", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + expect(state.outputs.windsurf_url.value).toBe( + "windsurf://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", + ); + + const coder_app = state.resources.find( + (res) => res.type === "coder_app" && res.name === "windsurf", + ); + + expect(coder_app).not.toBeNull(); + expect(coder_app?.instances.length).toBe(1); + expect(coder_app?.instances[0].attributes.order).toBeNull(); + }); + + it("adds folder", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + folder: "/foo/bar", + }); + expect(state.outputs.windsurf_url.value).toBe( + "windsurf://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", + ); + }); + + it("adds folder and open_recent", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + folder: "/foo/bar", + open_recent: true, + }); + expect(state.outputs.windsurf_url.value).toBe( + "windsurf://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", + ); + }); + + it("adds folder but not open_recent", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + folder: "/foo/bar", + open_recent: false, + }); + expect(state.outputs.windsurf_url.value).toBe( + "windsurf://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", + ); + }); + + it("adds open_recent", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + open_recent: true, + }); + expect(state.outputs.windsurf_url.value).toBe( + "windsurf://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", + ); + }); + + it("expect order to be set", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + order: 22, + }); + + const coder_app = state.resources.find( + (res) => res.type === "coder_app" && res.name === "windsurf", + ); + + expect(coder_app).not.toBeNull(); + expect(coder_app?.instances.length).toBe(1); + expect(coder_app?.instances[0].attributes.order).toBe(22); + }); +}); diff --git a/windsurf/main.tf b/windsurf/main.tf new file mode 100644 index 00000000..1d836d7e --- /dev/null +++ b/windsurf/main.tf @@ -0,0 +1,62 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.23" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "folder" { + type = string + description = "The folder to open in Cursor IDE." + default = "" +} + +variable "open_recent" { + type = bool + description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open." + default = false +} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_app" "windsurf" { + agent_id = var.agent_id + external = true + icon = "/icon/windsurf.svg" + slug = "windsurf" + display_name = "Windsurf Editor" + order = var.order + url = join("", [ + "windsurf://coder.coder-remote/open", + "?owner=", + data.coder_workspace_owner.me.name, + "&workspace=", + data.coder_workspace.me.name, + var.folder != "" ? join("", ["&folder=", var.folder]) : "", + var.open_recent ? "&openRecent" : "", + "&url=", + data.coder_workspace.me.access_url, + "&token=$SESSION_TOKEN", + ]) +} + +output "windsurf_url" { + value = coder_app.windsurf.url + description = "Windsurf Editor URL." +} From cc567ed5c48131b006a198f2bf162b8debec01f3 Mon Sep 17 00:00:00 2001 From: Chris Golden <551285+cirego@users.noreply.github.com> Date: Wed, 23 Apr 2025 23:31:53 -0700 Subject: [PATCH 29/41] fix(jupyterlab): fix path to jupyter-lab if already installed (#447) Co-authored-by: M Atif Ali --- jupyterlab/README.md | 2 +- jupyterlab/run.sh | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/jupyterlab/README.md b/jupyterlab/README.md index 8c2af03f..abebdc82 100644 --- a/jupyterlab/README.md +++ b/jupyterlab/README.md @@ -17,7 +17,7 @@ A module that adds JupyterLab in your Coder template. module "jupyterlab" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jupyterlab/coder" - version = "1.0.30" + version = "1.0.31" agent_id = coder_agent.example.id } ``` diff --git a/jupyterlab/run.sh b/jupyterlab/run.sh index 2dd34ace..be686e55 100755 --- a/jupyterlab/run.sh +++ b/jupyterlab/run.sh @@ -34,21 +34,22 @@ if ! command -v jupyter-lab > /dev/null 2>&1; then uv) uv pip install -q jupyterlab \ && printf "%s\n" "đŸĨŗ jupyterlab has been installed" - JUPYTERPATH="$HOME/.venv/bin/" + JUPYTER="$HOME/.venv/bin/jupyter-lab" ;; pipx) pipx install jupyterlab \ && printf "%s\n" "đŸĨŗ jupyterlab has been installed" - JUPYTERPATH="$HOME/.local/bin" + JUPYTER="$HOME/.local/bin/jupyter-lab" ;; esac else printf "%s\n\n" "đŸĨŗ jupyterlab is already installed" + JUPYTER=$(command -v jupyter-lab) fi printf "👷 Starting jupyterlab in background..." printf "check logs at ${LOG_PATH}" -$JUPYTERPATH/jupyter-lab --no-browser \ +$JUPYTER --no-browser \ "$BASE_URL_FLAG" \ --ServerApp.ip='*' \ --ServerApp.port="${PORT}" \ From 48dbe98bb513341a79fb298d7b21ce08de8b31ad Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 24 Apr 2025 07:02:50 -0700 Subject: [PATCH 30/41] fix(claude-code): add `dangerously-skip-permissions` (#442) Co-authored-by: Ben Potter Co-authored-by: Benjamin --- claude-code/README.md | 4 ++-- claude-code/main.tf | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/claude-code/README.md b/claude-code/README.md index e10281bb..7ff2ae87 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -14,7 +14,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/modules/claude-code/coder" - version = "1.2.0" + version = "1.2.1" agent_id = coder_agent.example.id folder = "/home/coder" install_claude_code = true @@ -102,7 +102,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude ```tf module "claude-code" { source = "registry.coder.com/modules/claude-code/coder" - version = "1.2.0" + version = "1.2.1" agent_id = coder_agent.example.id folder = "/home/coder" install_claude_code = true diff --git a/claude-code/main.tf b/claude-code/main.tf index 281b6c35..cc7b27e0 100644 --- a/claude-code/main.tf +++ b/claude-code/main.tf @@ -151,7 +151,7 @@ resource "coder_script" "claude_code" { export LC_ALL=en_US.UTF-8 # Create a new tmux session in detached mode - tmux new-session -d -s claude-code -c ${var.folder} "claude" + tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions" # Send the prompt to the tmux session if needed if [ -n "$CODER_MCP_CLAUDE_TASK_PROMPT" ]; then From 3c81321f3b4d136e20ba50ba475be39c78b2534b Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:41:46 -0500 Subject: [PATCH 31/41] ci: add deploy to main trigger (#448) This PR adds the trigger to auto deploy the prod registry alongside dev when we do a merge to main or tag a release. --- .github/workflows/deploy-registry.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy-registry.yaml b/.github/workflows/deploy-registry.yaml index bbecad96..ff32365e 100644 --- a/.github/workflows/deploy-registry.yaml +++ b/.github/workflows/deploy-registry.yaml @@ -36,4 +36,8 @@ jobs: - name: Deploy to dev.registry.coder.com run: | gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch dev + + - name: Deploy to registry.coder.com + run: | + gcloud builds triggers run 106610ff-41fb-4bd0-90a2-7643583fb9c0 --branch main From 284dcdee1b4f3e96bb3b8d6d72c28463ce1c9030 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:43:23 +0500 Subject: [PATCH 32/41] chore: bump crate-ci/typos from 1.31.1 to 1.31.2 (#455) 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 f0c2425a..a0cf49a9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,7 +48,7 @@ jobs: - name: Format run: bun fmt:ci - name: typos-action - uses: crate-ci/typos@v1.31.1 + uses: crate-ci/typos@v1.31.2 with: config: .github/typos.toml - name: Lint From 8eecf815734bae94acf2587ad51fd72b425f38e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:44:31 +0500 Subject: [PATCH 33/41] chore: bump google-github-actions/auth from 2.1.8 to 2.1.10 (#454) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-registry.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-registry.yaml b/.github/workflows/deploy-registry.yaml index ff32365e..bc60c06b 100644 --- a/.github/workflows/deploy-registry.yaml +++ b/.github/workflows/deploy-registry.yaml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v4 - name: Authenticate to Google Cloud - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 + uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193 with: workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com From f285eef4fe7cf67edc2c6b88c3226a1d5399c730 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 2 May 2025 12:53:21 +0100 Subject: [PATCH 34/41] fix(devcontainers-cli): install to `$CODER_SCRIPT_BIN_DIR` when using yarn (#457) When using `codercom/enterprise-node`, this will install the `devcontainer` cli into `/home/coder/.yarn/bin`, which is _not_ in `$PATH`. To work around this, we'll install it into `$CODER_SCRIPT_BIN_DIR`, like we do with `pnpm`. --- devcontainers-cli/README.md | 2 +- devcontainers-cli/run.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/devcontainers-cli/README.md b/devcontainers-cli/README.md index 11961cd7..a416bcd6 100644 --- a/devcontainers-cli/README.md +++ b/devcontainers-cli/README.md @@ -16,7 +16,7 @@ The devcontainers-cli module provides an easy way to install [`@devcontainers/cl ```tf module "devcontainers-cli" { source = "registry.coder.com/modules/devcontainers-cli/coder" - version = "1.0.1" + version = "1.0.2" agent_id = coder_agent.example.id } ``` diff --git a/devcontainers-cli/run.sh b/devcontainers-cli/run.sh index 03aac17f..c2543821 100755 --- a/devcontainers-cli/run.sh +++ b/devcontainers-cli/run.sh @@ -38,7 +38,7 @@ install() { fi pnpm add -g @devcontainers/cli elif [ "$PACKAGE_MANAGER" = "yarn" ]; then - yarn global add @devcontainers/cli + yarn global add @devcontainers/cli --prefix "$CODER_SCRIPT_BIN_DIR" fi } From c099b3e5ddcd2310d5bdb849475e1a451e257885 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 2 May 2025 18:10:09 +0100 Subject: [PATCH 35/41] fix(devcontainers-cli): set yarn install prefix to be one level up (#458) Unfortunately I missed that `--prefix=/tmp/coder-script-data/bin` will install into `/tmp/coder-script-data/bin/bin`. This asks for the parent directory, resulting in `--prefix=/tmp/coder-script-data`. --- devcontainers-cli/README.md | 2 +- devcontainers-cli/main.test.ts | 18 ++++++++++++++++-- devcontainers-cli/run.sh | 2 +- test.ts | 3 ++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/devcontainers-cli/README.md b/devcontainers-cli/README.md index a416bcd6..4b445073 100644 --- a/devcontainers-cli/README.md +++ b/devcontainers-cli/README.md @@ -16,7 +16,7 @@ The devcontainers-cli module provides an easy way to install [`@devcontainers/cl ```tf module "devcontainers-cli" { source = "registry.coder.com/modules/devcontainers-cli/coder" - version = "1.0.2" + version = "1.0.3" agent_id = coder_agent.example.id } ``` diff --git a/devcontainers-cli/main.test.ts b/devcontainers-cli/main.test.ts index 85686c46..892d6430 100644 --- a/devcontainers-cli/main.test.ts +++ b/devcontainers-cli/main.test.ts @@ -40,7 +40,21 @@ const executeScriptInContainerWithPackageManager = async ( ]); } - const resp = await execContainer(id, [shell, "-c", instance.script]); + const pathResp = await execContainer(id, [shell, "-c", "echo $PATH"]); + const path = pathResp.stdout.trim(); + + console.log(path); + + const resp = await execContainer( + id, + [shell, "-c", instance.script], + [ + "--env", + "CODER_SCRIPT_BIN_DIR=/tmp/coder-script-data/bin", + "--env", + `PATH=${path}:/tmp/coder-script-data/bin`, + ], + ); const stdout = resp.stdout.trim().split("\n"); const stderr = resp.stderr.trim().split("\n"); return { @@ -104,7 +118,7 @@ describe("devcontainers-cli", async () => { "Installing @devcontainers/cli using yarn...", ); expect(output.stdout[output.stdout.length - 1]).toEqual( - "đŸĨŗ @devcontainers/cli has been installed into /usr/local/bin/devcontainer!", + "đŸĨŗ @devcontainers/cli has been installed into /tmp/coder-script-data/bin/devcontainer!", ); }, 15000); diff --git a/devcontainers-cli/run.sh b/devcontainers-cli/run.sh index c2543821..bd3c1b1d 100755 --- a/devcontainers-cli/run.sh +++ b/devcontainers-cli/run.sh @@ -38,7 +38,7 @@ install() { fi pnpm add -g @devcontainers/cli elif [ "$PACKAGE_MANAGER" = "yarn" ]; then - yarn global add @devcontainers/cli --prefix "$CODER_SCRIPT_BIN_DIR" + yarn global add @devcontainers/cli --prefix "$(dirname "$CODER_SCRIPT_BIN_DIR")" fi } diff --git a/test.ts b/test.ts index b1b8aa83..e466cb12 100644 --- a/test.ts +++ b/test.ts @@ -66,12 +66,13 @@ export const executeScriptInContainer = async ( export const execContainer = async ( id: string, cmd: string[], + args?: string[], ): Promise<{ exitCode: number; stderr: string; stdout: string; }> => { - const proc = spawn(["docker", "exec", id, ...cmd], { + const proc = spawn(["docker", "exec", ...(args ?? []), id, ...cmd], { stderr: "pipe", stdout: "pipe", }); From c200eb5fd352b3d6c6ea8e2eab3f17c4e3061222 Mon Sep 17 00:00:00 2001 From: Matt Wise Date: Sat, 3 May 2025 15:33:32 -0700 Subject: [PATCH 36/41] feat(jetbrains-gateway): add arm64 support (#452) Provide support for running Jetbrains Gateway connections to both ARM64 and AMD64 hosts. --------- Co-authored-by: Phorcys <57866459+phorcys420@users.noreply.github.com> --- jetbrains-gateway/README.md | 12 ++--- jetbrains-gateway/main.tf | 88 +++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 34 deletions(-) diff --git a/jetbrains-gateway/README.md b/jetbrains-gateway/README.md index 73c1e128..fed0c083 100644 --- a/jetbrains-gateway/README.md +++ b/jetbrains-gateway/README.md @@ -18,7 +18,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.28" + version = "1.0.29" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"] @@ -36,7 +36,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.28" + version = "1.0.29" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["GO", "WS"] @@ -50,7 +50,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.28" + version = "1.0.29" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["IU", "PY"] @@ -65,7 +65,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.28" + version = "1.0.29" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["IU", "PY"] @@ -90,7 +90,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.28" + version = "1.0.29" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["GO", "WS"] @@ -108,7 +108,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.28" + version = "1.0.29" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["GO", "WS"] diff --git a/jetbrains-gateway/main.tf b/jetbrains-gateway/main.tf index d197399d..502469f2 100644 --- a/jetbrains-gateway/main.tf +++ b/jetbrains-gateway/main.tf @@ -13,6 +13,16 @@ terraform { } } +variable "arch" { + type = string + description = "The target architecture of the workspace" + default = "amd64" + validation { + condition = contains(["amd64", "arm64"], var.arch) + error_message = "Architecture must be either 'amd64' or 'arm64'." + } +} + variable "agent_id" { type = string description = "The ID of a Coder agent." @@ -178,78 +188,100 @@ data "http" "jetbrains_ide_versions" { } locals { + # AMD64 versions of the images just use the version string, while ARM64 + # versions append "-aarch64". Eg: + # + # https://download.jetbrains.com/idea/ideaIU-2025.1.tar.gz + # https://download.jetbrains.com/idea/ideaIU-2025.1.tar.gz + # + # We rewrite the data map above dynamically based on the user's architecture parameter. + # + effective_jetbrains_ide_versions = { + for k, v in var.jetbrains_ide_versions : k => { + build_number = v.build_number + version = var.arch == "arm64" ? "${v.version}-aarch64" : v.version + } + } + + # When downloading the latest IDE, the download link in the JSON is either: + # + # linux.download_link + # linuxARM64.download_link + # + download_key = var.arch == "arm64" ? "linuxARM64" : "linux" + jetbrains_ides = { "GO" = { icon = "/icon/goland.svg", name = "GoLand", identifier = "GO", - build_number = var.jetbrains_ide_versions["GO"].build_number, - download_link = "${var.download_base_link}/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz" - version = var.jetbrains_ide_versions["GO"].version + build_number = local.effective_jetbrains_ide_versions["GO"].build_number, + download_link = "${var.download_base_link}/go/goland-${local.effective_jetbrains_ide_versions["GO"].version}.tar.gz" + version = local.effective_jetbrains_ide_versions["GO"].version }, "WS" = { icon = "/icon/webstorm.svg", name = "WebStorm", identifier = "WS", - build_number = var.jetbrains_ide_versions["WS"].build_number, - download_link = "${var.download_base_link}/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz" - version = var.jetbrains_ide_versions["WS"].version + build_number = local.effective_jetbrains_ide_versions["WS"].build_number, + download_link = "${var.download_base_link}/webstorm/WebStorm-${local.effective_jetbrains_ide_versions["WS"].version}.tar.gz" + version = local.effective_jetbrains_ide_versions["WS"].version }, "IU" = { icon = "/icon/intellij.svg", name = "IntelliJ IDEA Ultimate", identifier = "IU", - build_number = var.jetbrains_ide_versions["IU"].build_number, - download_link = "${var.download_base_link}/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz" - version = var.jetbrains_ide_versions["IU"].version + build_number = local.effective_jetbrains_ide_versions["IU"].build_number, + download_link = "${var.download_base_link}/idea/ideaIU-${local.effective_jetbrains_ide_versions["IU"].version}.tar.gz" + version = local.effective_jetbrains_ide_versions["IU"].version }, "PY" = { icon = "/icon/pycharm.svg", name = "PyCharm Professional", identifier = "PY", - build_number = var.jetbrains_ide_versions["PY"].build_number, - download_link = "${var.download_base_link}/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz" - version = var.jetbrains_ide_versions["PY"].version + build_number = local.effective_jetbrains_ide_versions["PY"].build_number, + download_link = "${var.download_base_link}/python/pycharm-professional-${local.effective_jetbrains_ide_versions["PY"].version}.tar.gz" + version = local.effective_jetbrains_ide_versions["PY"].version }, "CL" = { icon = "/icon/clion.svg", name = "CLion", identifier = "CL", - build_number = var.jetbrains_ide_versions["CL"].build_number, - download_link = "${var.download_base_link}/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz" - version = var.jetbrains_ide_versions["CL"].version + build_number = local.effective_jetbrains_ide_versions["CL"].build_number, + download_link = "${var.download_base_link}/cpp/CLion-${local.effective_jetbrains_ide_versions["CL"].version}.tar.gz" + version = local.effective_jetbrains_ide_versions["CL"].version }, "PS" = { icon = "/icon/phpstorm.svg", name = "PhpStorm", identifier = "PS", - build_number = var.jetbrains_ide_versions["PS"].build_number, - download_link = "${var.download_base_link}/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz" - version = var.jetbrains_ide_versions["PS"].version + build_number = local.effective_jetbrains_ide_versions["PS"].build_number, + download_link = "${var.download_base_link}/webide/PhpStorm-${local.effective_jetbrains_ide_versions["PS"].version}.tar.gz" + version = local.effective_jetbrains_ide_versions["PS"].version }, "RM" = { icon = "/icon/rubymine.svg", name = "RubyMine", identifier = "RM", - build_number = var.jetbrains_ide_versions["RM"].build_number, - download_link = "${var.download_base_link}/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz" - version = var.jetbrains_ide_versions["RM"].version + build_number = local.effective_jetbrains_ide_versions["RM"].build_number, + download_link = "${var.download_base_link}/ruby/RubyMine-${local.effective_jetbrains_ide_versions["RM"].version}.tar.gz" + version = local.effective_jetbrains_ide_versions["RM"].version }, "RD" = { icon = "/icon/rider.svg", name = "Rider", identifier = "RD", - build_number = var.jetbrains_ide_versions["RD"].build_number, - download_link = "${var.download_base_link}/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz" - version = var.jetbrains_ide_versions["RD"].version + build_number = local.effective_jetbrains_ide_versions["RD"].build_number, + download_link = "${var.download_base_link}/rider/JetBrains.Rider-${local.effective_jetbrains_ide_versions["RD"].version}.tar.gz" + version = local.effective_jetbrains_ide_versions["RD"].version }, "RR" = { icon = "/icon/rustrover.svg", name = "RustRover", identifier = "RR", - build_number = var.jetbrains_ide_versions["RR"].build_number, - download_link = "${var.download_base_link}/rustrover/RustRover-${var.jetbrains_ide_versions["RR"].version}.tar.gz" - version = var.jetbrains_ide_versions["RR"].version + build_number = local.effective_jetbrains_ide_versions["RR"].build_number, + download_link = "${var.download_base_link}/rustrover/RustRover-${local.effective_jetbrains_ide_versions["RR"].version}.tar.gz" + version = local.effective_jetbrains_ide_versions["RR"].version } } @@ -258,7 +290,7 @@ locals { key = var.latest ? keys(local.json_data)[0] : "" display_name = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].name identifier = data.coder_parameter.jetbrains_ide.value - download_link = var.latest ? local.json_data[local.key][0].downloads.linux.link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link + download_link = var.latest ? local.json_data[local.key][0].downloads[local.download_key].link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link build_number = var.latest ? local.json_data[local.key][0].build : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number version = var.latest ? local.json_data[local.key][0].version : var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version } From 7a5fd4420095ad4e268008c65b4d4ee238bcd040 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 05:22:25 +0500 Subject: [PATCH 37/41] chore: bump crate-ci/typos from 1.31.2 to 1.32.0 (#460) 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 a0cf49a9..63c6062b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,7 +48,7 @@ jobs: - name: Format run: bun fmt:ci - name: typos-action - uses: crate-ci/typos@v1.31.2 + uses: crate-ci/typos@v1.32.0 with: config: .github/typos.toml - name: Lint From f4a10da727e51c6de8c51e404df48761be3a682d Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Wed, 7 May 2025 09:57:53 -0700 Subject: [PATCH 38/41] chore: bump minor version number of jetbrains-gateway (#463) --- jetbrains-gateway/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jetbrains-gateway/README.md b/jetbrains-gateway/README.md index fed0c083..f3fc33f7 100644 --- a/jetbrains-gateway/README.md +++ b/jetbrains-gateway/README.md @@ -18,7 +18,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.29" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"] @@ -36,7 +36,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.29" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["GO", "WS"] @@ -50,7 +50,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.29" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["IU", "PY"] @@ -65,7 +65,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.29" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["IU", "PY"] @@ -90,7 +90,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.29" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["GO", "WS"] @@ -108,7 +108,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.29" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["GO", "WS"] From b14a03a5a650cd1396f9d0ba05c979c0d26b526a Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Wed, 7 May 2025 09:58:00 -0700 Subject: [PATCH 39/41] chore: bump vault-jwt version to 1.0.31 to avoid a version conflict (#464) --- vault-jwt/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vault-jwt/README.md b/vault-jwt/README.md index 6da29124..6e77dda0 100644 --- a/vault-jwt/README.md +++ b/vault-jwt/README.md @@ -16,7 +16,7 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec module "vault" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vault-jwt/coder" - version = "1.0.21" + version = "1.0.31" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_jwt_role = "coder" # The Vault role to use for authentication @@ -43,7 +43,7 @@ curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/d module "vault" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vault-jwt/coder" - version = "1.0.21" + version = "1.0.31" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_jwt_auth_path = "oidc" @@ -59,7 +59,7 @@ data "coder_workspace_owner" "me" {} module "vault" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vault-jwt/coder" - version = "1.0.21" + version = "1.0.31" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_jwt_role = data.coder_workspace_owner.me.groups[0] @@ -72,7 +72,7 @@ module "vault" { module "vault" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/vault-jwt/coder" - version = "1.0.21" + version = "1.0.31" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_jwt_role = "coder" # The Vault role to use for authentication From 43ed02fb8a3756bec01223b7f6ceaef032405a20 Mon Sep 17 00:00:00 2001 From: Birdie Kingston Date: Thu, 8 May 2025 22:44:30 +1000 Subject: [PATCH 40/41] feat(vault-jwt): allow specifying the vault jwt token directly (#436) Co-authored-by: Birdie K <5210502+moo-im-a-cow@users.noreply.github.com> Co-authored-by: DevCats Co-authored-by: DevCats Co-authored-by: M Atif Ali Co-authored-by: Mathias Fredriksson --- vault-jwt/README.md | 118 +++++++++++++++++++++++++++++++++++++++++--- vault-jwt/main.tf | 9 +++- 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/vault-jwt/README.md b/vault-jwt/README.md index 6e77dda0..1907dbf0 100644 --- a/vault-jwt/README.md +++ b/vault-jwt/README.md @@ -10,16 +10,17 @@ tags: [helper, integration, vault, jwt, oidc] # Hashicorp Vault Integration (JWT) -This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces by reusing the [OIDC](https://coder.com/docs/admin/users/oidc-auth) access token from Coder's OIDC authentication method. This requires configuring the Vault [JWT/OIDC](https://developer.hashicorp.com/vault/docs/auth/jwt#configuration) auth method. +This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces by reusing the [OIDC](https://coder.com/docs/admin/users/oidc-auth) access token from Coder's OIDC authentication method or another source of jwt token. This requires configuring the Vault [JWT/OIDC](https://developer.hashicorp.com/vault/docs/auth/jwt#configuration) auth method. ```tf module "vault" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/modules/vault-jwt/coder" - version = "1.0.31" - agent_id = coder_agent.example.id - vault_addr = "https://vault.example.com" - vault_jwt_role = "coder" # The Vault role to use for authentication + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/vault-jwt/coder" + version = "1.1.0" + agent_id = coder_agent.example.id + vault_addr = "https://vault.example.com" + vault_jwt_role = "coder" # The Vault role to use for authentication + vault_jwt_token = "eyJhbGciOiJIUzI1N..." # optional, if not present, defaults to user's oidc authentication token } ``` @@ -79,3 +80,106 @@ module "vault" { vault_cli_version = "1.17.5" } ``` + +### Use a custom JWT token + +```tf + +terraform { + required_providers { + jwt = { + source = "geektheripper/jwt" + version = "1.1.4" + } + time = { + source = "hashicorp/time" + version = "0.11.1" + } + } +} + + +resource "jwt_signed_token" "vault" { + count = data.coder_workspace.me.start_count + algorithm = "RS256" + # `openssl genrsa -out key.pem 4096` and `openssl rsa -in key.pem -pubout > pub.pem` to generate keys + key = file("key.pem") + claims_json = jsonencode({ + iss = "https://code.example.com" + sub = "${data.coder_workspace.me.id}" + aud = "https://vault.example.com" + iat = provider::time::rfc3339_parse(plantimestamp()).unix + # Uncomment to set an expiry on the JWT token(default 3600 seconds). + # workspace will need to be restarted to generate a new token if it expires + #exp = provider::time::rfc3339_parse(timeadd(timestamp(), 3600)).unix agent = coder_agent.main.id + provisioner = data.coder_provisioner.main.id + provisioner_arch = data.coder_provisioner.main.arch + provisioner_os = data.coder_provisioner.main.os + + workspace = data.coder_workspace.me.id + workspace_url = data.coder_workspace.me.access_url + workspace_port = data.coder_workspace.me.access_port + workspace_name = data.coder_workspace.me.name + template = data.coder_workspace.me.template_id + template_name = data.coder_workspace.me.template_name + template_version = data.coder_workspace.me.template_version + owner = data.coder_workspace_owner.me.id + owner_name = data.coder_workspace_owner.me.name + owner_email = data.coder_workspace_owner.me.email + owner_login_type = data.coder_workspace_owner.me.login_type + owner_groups = data.coder_workspace_owner.me.groups + }) +} + +module "vault" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/vault-jwt/coder" + version = "1.1.0" + agent_id = coder_agent.example.id + vault_addr = "https://vault.example.com" + vault_jwt_role = "coder" # The Vault role to use for authentication + vault_jwt_token = jwt_signed_token.vault[0].token +} +``` + +#### Example Vault JWT role + +```shell +vault write auth/JWT_MOUNT/role/workspace - << EOF +{ + "user_claim": "sub", + "bound_audiences": "https://vault.example.com", + "role_type": "jwt", + "ttl": "1h", + "claim_mappings": { + "owner": "owner", + "owner_email": "owner_email", + "owner_login_type": "owner_login_type", + "owner_name": "owner_name", + "provisioner": "provisioner", + "provisioner_arch": "provisioner_arch", + "provisioner_os": "provisioner_os", + "sub": "sub", + "template": "template", + "template_name": "template_name", + "template_version": "template_version", + "workspace": "workspace", + "workspace_name": "workspace_name", + "workspace_id": "workspace_id" + } +} +EOF +``` + +#### Example workspace access Vault policy + +```tf +path "kv/data/app/coder/{{identity.entity.aliases..metadata.owner_name}}/{{identity.entity.aliases..metadata.workspace_name}}" { + capabilities = ["create", "read", "update", "delete", "list", "subscribe"] + subscribe_event_types = ["*"] +} +path "kv/metadata/app/coder/{{identity.entity.aliases..metadata.owner_name}}/{{identity.entity.aliases..metadata.workspace_name}}" { + capabilities = ["create", "read", "update", "delete", "list", "subscribe"] + subscribe_event_types = ["*"] +} +``` diff --git a/vault-jwt/main.tf b/vault-jwt/main.tf index adcc34d4..17288e00 100644 --- a/vault-jwt/main.tf +++ b/vault-jwt/main.tf @@ -20,6 +20,13 @@ variable "vault_addr" { description = "The address of the Vault server." } +variable "vault_jwt_token" { + type = string + description = "The JWT token used for authentication with Vault." + default = null + sensitive = true +} + variable "vault_jwt_auth_path" { type = string description = "The path to the Vault JWT auth method." @@ -46,7 +53,7 @@ resource "coder_script" "vault" { display_name = "Vault (GitHub)" icon = "/icon/vault.svg" script = templatefile("${path.module}/run.sh", { - CODER_OIDC_ACCESS_TOKEN : data.coder_workspace_owner.me.oidc_access_token, + CODER_OIDC_ACCESS_TOKEN : var.vault_jwt_token != null ? var.vault_jwt_token : data.coder_workspace_owner.me.oidc_access_token, VAULT_JWT_AUTH_PATH : var.vault_jwt_auth_path, VAULT_JWT_ROLE : var.vault_jwt_role, VAULT_CLI_VERSION : var.vault_cli_version, From c873967932fa02f89d7c52beab7c3409de04026f Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Fri, 9 May 2025 09:57:02 -0700 Subject: [PATCH 41/41] chore: update README.md (#471) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index acec4e5d..81d8d380 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +> [!CAUTION] +> We are no longer accepting new contributions to this repo. We have moved all modules to https://github.com/coder/registry repo. Please see https://github.com/coder/modules/discussions/469 for more details. +

Modules