From a2318003c6adf3365c387a33f9258bfe16eac42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mu=C3=B1oz?= <2280164+rodmk@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:09:49 +0000 Subject: [PATCH 1/3] fix(code-server,vscode-web): parse JSONC in extensions.json with sed Co-Authored-By: Claude Opus 4.6 --- .github/typos.toml | 1 + registry/coder/modules/code-server/README.md | 16 ++++++++-------- registry/coder/modules/code-server/run.sh | 15 +++++++++++++-- registry/coder/modules/vscode-web/README.md | 12 ++++++------ registry/coder/modules/vscode-web/run.sh | 18 +++++++++++++++--- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/.github/typos.toml b/.github/typos.toml index d0b1f2863..73b31c30b 100644 --- a/.github/typos.toml +++ b/.github/typos.toml @@ -8,6 +8,7 @@ mavrickrishi = "mavrickrishi" # Username mavrick = "mavrick" # Username inh = "inh" # Option in setpriv command exportfs = "exportfs" # nfs related binary +ba = "ba" # sed branch command in :a;N;$!ba pattern [files] extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive \ No newline at end of file diff --git a/registry/coder/modules/code-server/README.md b/registry/coder/modules/code-server/README.md index fdb3f1a74..2d1c58e8b 100644 --- a/registry/coder/modules/code-server/README.md +++ b/registry/coder/modules/code-server/README.md @@ -14,7 +14,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/coder/code-server/coder" - version = "1.4.3" + version = "1.4.4" agent_id = coder_agent.example.id } ``` @@ -29,7 +29,7 @@ module "code-server" { module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.4.3" + version = "1.4.4" agent_id = coder_agent.example.id install_version = "4.106.3" } @@ -43,7 +43,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/coder/code-server/coder" - version = "1.4.3" + version = "1.4.4" agent_id = coder_agent.example.id extensions = [ "dracula-theme.theme-dracula" @@ -61,7 +61,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/coder/code-server/coder" - version = "1.4.3" + version = "1.4.4" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -78,7 +78,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/coder/code-server/coder" - version = "1.4.3" + version = "1.4.4" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] } @@ -92,7 +92,7 @@ You can pass additional command-line arguments to code-server using the `additio module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.4.3" + version = "1.4.4" agent_id = coder_agent.example.id additional_args = "--disable-workspace-trust" } @@ -108,7 +108,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/coder/code-server/coder" - version = "1.4.3" + version = "1.4.4" agent_id = coder_agent.example.id use_cached = true extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] @@ -121,7 +121,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/coder/code-server/coder" - version = "1.4.3" + version = "1.4.4" agent_id = coder_agent.example.id offline = true } diff --git a/registry/coder/modules/code-server/run.sh b/registry/coder/modules/code-server/run.sh index 33a6972a6..bf8585868 100644 --- a/registry/coder/modules/code-server/run.sh +++ b/registry/coder/modules/code-server/run.sh @@ -116,6 +116,17 @@ for extension in "$${EXTENSIONLIST[@]}"; do fi done +# Strip JSONC features (block/line comments, trailing commas) for jq parsing +strip_jsonc_for_extensions() { + sed 's|//.*||g' "$1" | sed -E ' + :a + N + $!ba + s#/[*]([^*]|[*]+[^*/])*[*]+/##g + s/,[^]}"]*([]}])/\1/g + ' +} + if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then if ! command -v jq > /dev/null; then echo "jq is required to install extensions from a workspace file." @@ -129,8 +140,8 @@ if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR" - # Use sed to remove single-line comments before parsing with jq - extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR"/.vscode/extensions.json | jq -r '.recommendations[]') + extensions=$(strip_jsonc_for_extensions "$WORKSPACE_DIR/.vscode/extensions.json" \ + | jq -r '(.recommendations // [])[]') for extension in $extensions; do if extension_installed "$extension"; then continue diff --git a/registry/coder/modules/vscode-web/README.md b/registry/coder/modules/vscode-web/README.md index 35cad1adb..8a622f993 100644 --- a/registry/coder/modules/vscode-web/README.md +++ b/registry/coder/modules/vscode-web/README.md @@ -14,7 +14,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/coder/vscode-web/coder" - version = "1.5.0" + version = "1.5.1" agent_id = coder_agent.example.id accept_license = true } @@ -30,7 +30,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.5.0" + version = "1.5.1" agent_id = coder_agent.example.id install_prefix = "/home/coder/.vscode-web" folder = "/home/coder" @@ -44,7 +44,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.5.0" + version = "1.5.1" agent_id = coder_agent.example.id extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"] accept_license = true @@ -59,7 +59,7 @@ Configure VS Code's [Machine settings.json](https://code.visualstudio.com/docs/g module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.5.0" + version = "1.5.1" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -80,7 +80,7 @@ By default, this module installs the latest. To pin a specific version, retrieve module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.5.0" + version = "1.5.1" agent_id = coder_agent.example.id commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447" accept_license = true @@ -96,7 +96,7 @@ Note: Either `workspace` or `folder` can be used, but not both simultaneously. T module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.5.0" + version = "1.5.1" agent_id = coder_agent.example.id workspace = "/home/coder/coder.code-workspace" } diff --git a/registry/coder/modules/vscode-web/run.sh b/registry/coder/modules/vscode-web/run.sh index dea8e5853..a78760cea 100644 --- a/registry/coder/modules/vscode-web/run.sh +++ b/registry/coder/modules/vscode-web/run.sh @@ -150,6 +150,17 @@ for extension in "$${EXTENSIONLIST[@]}"; do fi done +# Strip JSONC features (block/line comments, trailing commas) for jq parsing +strip_jsonc_for_extensions() { + sed 's|//.*||g' "$1" | sed -E ' + :a + N + $!ba + s#/[*]([^*]|[*]+[^*/])*[*]+/##g + s/,[^]}"]*([]}])/\1/g + ' +} + if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then if ! command -v jq > /dev/null; then echo "jq is required to install extensions from a workspace file." @@ -157,8 +168,8 @@ if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then # Prefer WORKSPACE if set and points to a file if [ -n "${WORKSPACE}" ] && [ -f "${WORKSPACE}" ]; then printf "🧩 Installing extensions from %s...\n" "${WORKSPACE}" - # Strip single-line comments then parse .extensions.recommendations[] - extensions=$(sed 's|//.*||g' "${WORKSPACE}" | jq -r '(.extensions.recommendations // [])[]') + extensions=$(strip_jsonc_for_extensions "${WORKSPACE}" \ + | jq -r '(.extensions.recommendations // [])[]') for extension in $extensions; do $VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force done @@ -170,7 +181,8 @@ if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then fi if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR" - extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR/.vscode/extensions.json" | jq -r '.recommendations[]') + extensions=$(strip_jsonc_for_extensions "$WORKSPACE_DIR/.vscode/extensions.json" \ + | jq -r '(.recommendations // [])[]') for extension in $extensions; do $VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force done From f29505abc74e7cd1dceed28aaa9a105d6a39aaba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mu=C3=B1oz?= Date: Thu, 9 Jul 2026 09:42:22 -0700 Subject: [PATCH 2/3] fix(code-server,vscode-web): correct JSONC strip ordering and null-safety Address review feedback on the strip_jsonc_for_extensions helper. The previous two-stage pipeline stripped line comments before slurping the file for block-comment removal. This corrupted any // that lived inside a block comment (e.g. a URL in /* see https://... */), leaving an unterminated /* that broke jq. Single-line input was also dropped entirely by the :a;N;$!ba slurp. Reorder into three portable passes so each concern gets the right scope: 1. block comments (whole-file slurp) first, so a URL inside /* ... */ is removed as a unit before the line-comment pass sees its // 2. line comments per line (no non-portable [^\n] class); // preceded by ':' is preserved so URLs in .code-workspace string values survive 3. trailing commas (whole-file slurp) The slurp idiom is :a;$!{N;ba}, which is single-line safe. Verified on GNU sed (Linux), BSD sed (macOS), and BusyBox sed (Alpine). Also make the code-server recommendations query null-safe to match vscode-web: .recommendations[] -> (.recommendations // [])[], so a valid extensions.json without a recommendations key no longer errors. Bump code-server 1.5.1 -> 1.5.2 and vscode-web 1.6.1 -> 1.6.2. Co-Authored-By: Claude Opus 4.8 (1M context) --- registry/coder/modules/code-server/README.md | 18 ++++----- registry/coder/modules/code-server/run.sh | 42 +++++++++++++++----- registry/coder/modules/vscode-web/README.md | 12 +++--- registry/coder/modules/vscode-web/run.sh | 38 ++++++++++++++---- 4 files changed, 77 insertions(+), 33 deletions(-) diff --git a/registry/coder/modules/code-server/README.md b/registry/coder/modules/code-server/README.md index 06dee1985..3a71da725 100644 --- a/registry/coder/modules/code-server/README.md +++ b/registry/coder/modules/code-server/README.md @@ -14,7 +14,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/coder/code-server/coder" - version = "1.5.1" + version = "1.5.2" agent_id = coder_agent.example.id } ``` @@ -29,7 +29,7 @@ module "code-server" { module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.5.1" + version = "1.5.2" agent_id = coder_agent.example.id install_version = "4.106.3" } @@ -43,7 +43,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/coder/code-server/coder" - version = "1.5.1" + version = "1.5.2" agent_id = coder_agent.example.id extensions = [ "dracula-theme.theme-dracula" @@ -61,7 +61,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/coder/code-server/coder" - version = "1.5.1" + version = "1.5.2" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -78,7 +78,7 @@ Install multiple extensions from [OpenVSX](https://open-vsx.org/) by adding them module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.5.1" + version = "1.5.2" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] } @@ -92,7 +92,7 @@ Open a [`.code-workspace`](https://coder.com/docs/code-server/FAQ#how-does-code- module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.5.1" + version = "1.5.2" agent_id = coder_agent.example.id workspace = "/home/coder/project/my.code-workspace" } @@ -106,7 +106,7 @@ You can pass additional command-line arguments to code-server using the `additio module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.5.1" + version = "1.5.2" agent_id = coder_agent.example.id additional_args = "--disable-workspace-trust" } @@ -122,7 +122,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/coder/code-server/coder" - version = "1.5.1" + version = "1.5.2" agent_id = coder_agent.example.id use_cached = true extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] @@ -135,7 +135,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/coder/code-server/coder" - version = "1.5.1" + version = "1.5.2" agent_id = coder_agent.example.id offline = true } diff --git a/registry/coder/modules/code-server/run.sh b/registry/coder/modules/code-server/run.sh index d55b0e548..a17e2236f 100644 --- a/registry/coder/modules/code-server/run.sh +++ b/registry/coder/modules/code-server/run.sh @@ -116,15 +116,37 @@ for extension in "$${EXTENSIONLIST[@]}"; do fi done -# Strip JSONC features (block/line comments, trailing commas) for jq parsing +# Strip JSONC features (block/line comments, trailing commas) so jq can parse +# .vscode/extensions.json and .code-workspace files. Portable across GNU, BSD, +# and BusyBox sed (Coder workspaces run on Linux, macOS and Alpine). +# +# Three passes, because each concern needs a different scope and order: +# 1. Block comments - slurps the whole file so /* ... */ can span lines. +# Runs first so a URL such as /* see https://example */ is removed as a +# unit and its // is never seen by the line-comment pass. +# 2. Line comments - per line, so // ... stops at end of line without the +# non-portable [^\n] class (BSD sed reads \n inside a bracket as a literal +# backslash and n, silently corrupting IDs containing "n"). A // preceded +# by ':' is preserved so URLs in string values (e.g. proxy settings in a +# .code-workspace) survive. +# 3. Trailing commas - slurps the whole file so a comma and its closing +# bracket may sit on different lines. +# The ':a;$!{N;ba}' slurp is single-line safe (it falls through on the last or +# only line), unlike ':a;N;$!ba', which drops single-line input entirely. strip_jsonc_for_extensions() { - sed 's|//.*||g' "$1" | sed -E ' - :a - N - $!ba - s#/[*]([^*]|[*]+[^*/])*[*]+/##g - s/,[^]}"]*([]}])/\1/g - ' + sed -E ':a +$!{ +N +ba +} +s#/[*]([^*]|[*]+[^*/])*[*]+/##g' "$1" \ + | sed -E 's#^[[:space:]]*//.*##; s#([^:])//.*#\1#' \ + | sed -E ':a +$!{ +N +ba +} +s/,[^]}"]*([]}])/\1/g' } if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then @@ -134,11 +156,11 @@ if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then fi RECOMMENDATIONS_FILE="" - RECOMMENDATIONS_QUERY=".recommendations[]" + RECOMMENDATIONS_QUERY='(.recommendations // [])[]' if [ -n "${WORKSPACE}" ]; then if [ -f "${WORKSPACE}" ]; then RECOMMENDATIONS_FILE="${WORKSPACE}" - RECOMMENDATIONS_QUERY=".extensions.recommendations[]?" + RECOMMENDATIONS_QUERY='(.extensions.recommendations // [])[]' else echo "⚠️ Workspace file ${WORKSPACE} not found, skipping extension recommendations." fi diff --git a/registry/coder/modules/vscode-web/README.md b/registry/coder/modules/vscode-web/README.md index e3a1681ea..08b952b5a 100644 --- a/registry/coder/modules/vscode-web/README.md +++ b/registry/coder/modules/vscode-web/README.md @@ -14,7 +14,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/coder/vscode-web/coder" - version = "1.6.1" + version = "1.6.2" agent_id = coder_agent.example.id accept_license = true } @@ -30,7 +30,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.6.1" + version = "1.6.2" agent_id = coder_agent.example.id install_prefix = "/home/coder/.vscode-web" folder = "/home/coder" @@ -44,7 +44,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.6.1" + version = "1.6.2" agent_id = coder_agent.example.id extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"] accept_license = true @@ -59,7 +59,7 @@ Configure VS Code's [Machine settings.json](https://code.visualstudio.com/docs/g module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.6.1" + version = "1.6.2" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -80,7 +80,7 @@ By default, this module installs the latest. To pin a specific version, retrieve module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.6.1" + version = "1.6.2" agent_id = coder_agent.example.id commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447" accept_license = true @@ -96,7 +96,7 @@ Note: Either `workspace` or `folder` can be used, but not both simultaneously. T module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.6.1" + version = "1.6.2" agent_id = coder_agent.example.id workspace = "/home/coder/coder.code-workspace" } diff --git a/registry/coder/modules/vscode-web/run.sh b/registry/coder/modules/vscode-web/run.sh index a78760cea..0204cafa2 100644 --- a/registry/coder/modules/vscode-web/run.sh +++ b/registry/coder/modules/vscode-web/run.sh @@ -150,15 +150,37 @@ for extension in "$${EXTENSIONLIST[@]}"; do fi done -# Strip JSONC features (block/line comments, trailing commas) for jq parsing +# Strip JSONC features (block/line comments, trailing commas) so jq can parse +# .vscode/extensions.json and .code-workspace files. Portable across GNU, BSD, +# and BusyBox sed (Coder workspaces run on Linux, macOS and Alpine). +# +# Three passes, because each concern needs a different scope and order: +# 1. Block comments - slurps the whole file so /* ... */ can span lines. +# Runs first so a URL such as /* see https://example */ is removed as a +# unit and its // is never seen by the line-comment pass. +# 2. Line comments - per line, so // ... stops at end of line without the +# non-portable [^\n] class (BSD sed reads \n inside a bracket as a literal +# backslash and n, silently corrupting IDs containing "n"). A // preceded +# by ':' is preserved so URLs in string values (e.g. proxy settings in a +# .code-workspace) survive. +# 3. Trailing commas - slurps the whole file so a comma and its closing +# bracket may sit on different lines. +# The ':a;$!{N;ba}' slurp is single-line safe (it falls through on the last or +# only line), unlike ':a;N;$!ba', which drops single-line input entirely. strip_jsonc_for_extensions() { - sed 's|//.*||g' "$1" | sed -E ' - :a - N - $!ba - s#/[*]([^*]|[*]+[^*/])*[*]+/##g - s/,[^]}"]*([]}])/\1/g - ' + sed -E ':a +$!{ +N +ba +} +s#/[*]([^*]|[*]+[^*/])*[*]+/##g' "$1" \ + | sed -E 's#^[[:space:]]*//.*##; s#([^:])//.*#\1#' \ + | sed -E ':a +$!{ +N +ba +} +s/,[^]}"]*([]}])/\1/g' } if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then From b0fc629a25130f8d63afe21d3252173b17b5f4b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mu=C3=B1oz?= Date: Thu, 9 Jul 2026 12:13:01 -0700 Subject: [PATCH 3/3] test(code-server,vscode-web): cover JSONC extension parsing Add container-based tests that drive the rendered coder_script through the auto_install_extensions path with a mock CLI that records --install-extension calls, so the actual strip_jsonc_for_extensions + jq queries are exercised. - code-server: reaches auto-install via use_cached + a pre-placed mock binary. Covers a JSONC .vscode/extensions.json (line/block/multi-line comments, a URL inside a block comment, trailing comma) and a file with no recommendations key (null-safe query must not make jq error). - vscode-web: early-exits on use_cached/offline, so curl/tar are stubbed to make the download a no-op and let the pre-placed mock binary survive. Covers the JSONC .vscode/extensions.json path and a .code-workspace whose settings hold a URL (the :// guard must keep it intact so jq can parse the workspace file). These fail on the pre-fix code (the old pipeline drops the extension after the URL-bearing block comment; the old code-server query errors on a missing key). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../coder/modules/code-server/main.test.ts | 124 +++++++++++++++ .../coder/modules/vscode-web/main.test.ts | 149 ++++++++++++++++++ 2 files changed, 273 insertions(+) diff --git a/registry/coder/modules/code-server/main.test.ts b/registry/coder/modules/code-server/main.test.ts index 2686fa1e9..087d4d8df 100644 --- a/registry/coder/modules/code-server/main.test.ts +++ b/registry/coder/modules/code-server/main.test.ts @@ -18,6 +18,39 @@ import { setDefaultTimeout(2 * 60 * 1000); +// Mock code-server CLI that records every `--install-extension ` call as +// "INSTALLED:" on stdout so tests can assert which extensions the script +// tried to install. `--list-extensions` returns nothing (no cached extensions). +const MOCK_CODE_SERVER = `#!/bin/bash +if [ "$1" = "--list-extensions" ]; then + exit 0 +fi +prev="" +for arg in "$@"; do + if [ "$prev" = "--install-extension" ]; then + echo "INSTALLED:$arg" + fi + prev="$arg" +done +exit 0`; + +// A .vscode/extensions.json exercising every JSONC feature the stripper must +// handle: a standalone line comment, an end-of-line comment, a block comment +// containing a URL (the case the previous sed pipeline corrupted), a multi-line +// block comment, and a trailing comma. +const JSONC_EXTENSIONS_JSON = `{ + // Recommended extensions for this workspace + "recommendations": [ + "ms-python.python", // Python language support + /* linting - see https://open-vsx.org for the registry */ + "dbaeumer.vscode-eslint", + /* + * Formatting tools + */ + "esbenp.prettier-vscode", // trailing comma below is intentional + ] +}`; + let cleanupContainers: string[] = []; afterEach(async () => { @@ -214,6 +247,97 @@ chmod +x /tmp/code-server/bin/code-server`, expect(settingsResult.stdout).toContain("template_value"); }); + it("auto-installs recommended extensions from a JSONC extensions.json", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + use_cached: true, + auto_install_extensions: true, + }); + + const containerId = await runContainer("ubuntu:22.04"); + cleanupContainers.push(containerId); + + await execContainer(containerId, ["apt-get", "update", "-qq"]); + await execContainer(containerId, ["apt-get", "install", "-y", "-qq", "jq"]); + + // use_cached + a pre-existing binary skips the real install so the script + // reaches the auto-install path. + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /tmp/code-server/bin && cat > /tmp/code-server/bin/code-server << 'MOCKEOF' +${MOCK_CODE_SERVER} +MOCKEOF +chmod +x /tmp/code-server/bin/code-server`, + ]); + + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /root/.vscode && cat > /root/.vscode/extensions.json << 'JSONCEOF' +${JSONC_EXTENSIONS_JSON} +JSONCEOF`, + ]); + + const script = findResourceInstance(state, "coder_script"); + const result = await execContainer(containerId, [ + "bash", + "-c", + script.script, + ]); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("INSTALLED:ms-python.python"); + expect(result.stdout).toContain("INSTALLED:dbaeumer.vscode-eslint"); + expect(result.stdout).toContain("INSTALLED:esbenp.prettier-vscode"); + }); + + it("does not error on an extensions.json without a recommendations key", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + use_cached: true, + auto_install_extensions: true, + }); + + const containerId = await runContainer("ubuntu:22.04"); + cleanupContainers.push(containerId); + + await execContainer(containerId, ["apt-get", "update", "-qq"]); + await execContainer(containerId, ["apt-get", "install", "-y", "-qq", "jq"]); + + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /tmp/code-server/bin && cat > /tmp/code-server/bin/code-server << 'MOCKEOF' +${MOCK_CODE_SERVER} +MOCKEOF +chmod +x /tmp/code-server/bin/code-server`, + ]); + + // Valid JSON, but no `recommendations` key. The null-safe query + // `(.recommendations // [])[]` must not make jq error on the missing key. + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /root/.vscode && cat > /root/.vscode/extensions.json << 'JSONCEOF' +{ + "unwantedRecommendations": ["ms-python.python"] +} +JSONCEOF`, + ]); + + const script = findResourceInstance(state, "coder_script"); + const result = await execContainer(containerId, [ + "bash", + "-c", + script.script, + ]); + + expect(result.exitCode).toBe(0); + expect(result.stdout).not.toContain("INSTALLED:"); + expect(result.stderr).not.toContain("Cannot iterate over null"); + }); + it("installs and runs code-server", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", diff --git a/registry/coder/modules/vscode-web/main.test.ts b/registry/coder/modules/vscode-web/main.test.ts index 96c787c86..5e140d92e 100644 --- a/registry/coder/modules/vscode-web/main.test.ts +++ b/registry/coder/modules/vscode-web/main.test.ts @@ -18,6 +18,51 @@ import { // Set timeout to 2 minutes for tests that install packages setDefaultTimeout(2 * 60 * 1000); +// Mock vscode-web CLI that records every `--install-extension ` call as +// "INSTALLED:" on stdout so tests can assert which extensions the script +// tried to install. +const MOCK_VSCODE_WEB = `#!/bin/bash +prev="" +for arg in "$@"; do + if [ "$prev" = "--install-extension" ]; then + echo "INSTALLED:$arg" + fi + prev="$arg" +done +exit 0`; + +// Stub curl/tar so the download is a no-op: the script only reaches the +// extension-install path when neither use_cached nor offline is set (both exit +// early), so the real download must be short-circuited while the pre-placed +// mock binary survives. +const STUB_DOWNLOAD = `cat > /usr/local/bin/curl << 'CURLEOF' +#!/bin/bash +echo '"stub-commit"' +CURLEOF +cat > /usr/local/bin/tar << 'TAREOF' +#!/bin/bash +cat > /dev/null 2>&1 || true +exit 0 +TAREOF +chmod +x /usr/local/bin/curl /usr/local/bin/tar`; + +// A .vscode/extensions.json exercising every JSONC feature the stripper must +// handle: a standalone line comment, an end-of-line comment, a block comment +// containing a URL (the case the previous sed pipeline corrupted), a multi-line +// block comment, and a trailing comma. +const JSONC_EXTENSIONS_JSON = `{ + // Recommended extensions for this workspace + "recommendations": [ + "ms-python.python", // Python language support + /* linting - see https://open-vsx.org for the registry */ + "dbaeumer.vscode-eslint", + /* + * Formatting tools + */ + "esbenp.prettier-vscode", // trailing comma below is intentional + ] +}`; + let cleanupContainers: string[] = []; afterEach(async () => { @@ -295,4 +340,108 @@ chmod +x /tmp/vscode-web/bin/code-server`, expect(settingsResult.stdout).not.toContain("new.setting"); expect(settingsResult.stdout).not.toContain("new_value"); }); + + it("auto-installs recommended extensions from a JSONC extensions.json", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + accept_license: true, + auto_install_extensions: true, + }); + + const containerId = await runContainer("ubuntu:22.04"); + cleanupContainers.push(containerId); + + await execContainer(containerId, ["apt-get", "update", "-qq"]); + await execContainer(containerId, ["apt-get", "install", "-y", "-qq", "jq"]); + + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF' +${MOCK_VSCODE_WEB} +MOCKEOF +chmod +x /tmp/vscode-web/bin/code-server +${STUB_DOWNLOAD}`, + ]); + + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /root/.vscode && cat > /root/.vscode/extensions.json << 'JSONCEOF' +${JSONC_EXTENSIONS_JSON} +JSONCEOF`, + ]); + + const script = findResourceInstance(state, "coder_script"); + const result = await execContainer(containerId, [ + "bash", + "-c", + script.script, + ]); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("INSTALLED:ms-python.python"); + expect(result.stdout).toContain("INSTALLED:dbaeumer.vscode-eslint"); + expect(result.stdout).toContain("INSTALLED:esbenp.prettier-vscode"); + }); + + it("auto-installs extensions from a JSONC .code-workspace with URL-valued settings", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + accept_license: true, + auto_install_extensions: true, + workspace: "/root/team.code-workspace", + }); + + const containerId = await runContainer("ubuntu:22.04"); + cleanupContainers.push(containerId); + + await execContainer(containerId, ["apt-get", "update", "-qq"]); + await execContainer(containerId, ["apt-get", "install", "-y", "-qq", "jq"]); + + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF' +${MOCK_VSCODE_WEB} +MOCKEOF +chmod +x /tmp/vscode-web/bin/code-server +${STUB_DOWNLOAD}`, + ]); + + // A .code-workspace whose settings hold a URL. The `://` in the URL must + // survive JSONC stripping (it is not a comment) so jq can parse the file + // and read .extensions.recommendations. + await execContainer(containerId, [ + "bash", + "-c", + `cat > /root/team.code-workspace << 'JSONCEOF' +{ + // Team workspace configuration + "folders": [{ "path": "." }], + "settings": { + "http.proxy": "https://proxy.corp.example:8080", // corporate proxy URL + }, + "extensions": { + "recommendations": [ + "ms-python.python", + /* linting - https://open-vsx.org */ + "dbaeumer.vscode-eslint", + ] + } +} +JSONCEOF`, + ]); + + const script = findResourceInstance(state, "coder_script"); + const result = await execContainer(containerId, [ + "bash", + "-c", + script.script, + ]); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("INSTALLED:ms-python.python"); + expect(result.stdout).toContain("INSTALLED:dbaeumer.vscode-eslint"); + }); });