Skip to content
1 change: 1 addition & 0 deletions .github/typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 9 additions & 9 deletions registry/coder/modules/code-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```
Expand All @@ -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"
}
Expand All @@ -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"
Expand All @@ -61,7 +61,7 @@ Configure VS Code's [User settings.json](https://code.visualstudio.com/docs/gets
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 = {
Expand All @@ -81,7 +81,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"]
}
Expand All @@ -95,7 +95,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"
}
Expand All @@ -109,7 +109,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"
}
Expand All @@ -125,7 +125,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"]
Expand All @@ -138,7 +138,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
}
Expand Down
125 changes: 125 additions & 0 deletions registry/coder/modules/code-server/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,39 @@ import {

setDefaultTimeout(2 * 60 * 1000);

// Mock code-server CLI that records every `--install-extension <id>` call as
// "INSTALLED:<id>" 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 () => {
Expand Down Expand Up @@ -214,6 +247,98 @@ 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).toContain("Installing extensions from");
expect(result.stdout).not.toContain("INSTALLED:");
expect(result.stderr).not.toContain("jq: error");
});

it("installs and runs code-server", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
Expand Down
41 changes: 37 additions & 4 deletions registry/coder/modules/code-server/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,51 @@ for extension in "$${EXTENSIONLIST[@]}"; do
fi
done

# 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).
strip_jsonc_for_extensions() {
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
if ! command -v jq > /dev/null; then
echo "jq is required to install extensions from a workspace file."
exit 0
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
Expand All @@ -183,8 +216,8 @@ if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then

if [ -n "$RECOMMENDATIONS_FILE" ]; then
printf "🧩 Installing extensions from %s...\n" "$RECOMMENDATIONS_FILE"
# Use sed to remove single-line comments before parsing with jq
extensions=$(sed 's|//.*||g' "$RECOMMENDATIONS_FILE" | jq -r "$RECOMMENDATIONS_QUERY")
# Strip JSONC comments and trailing commas before parsing with jq
extensions=$(strip_jsonc_for_extensions "$RECOMMENDATIONS_FILE" | jq -r "$RECOMMENDATIONS_QUERY")
Comment thread
35C4n0r marked this conversation as resolved.
for extension in $extensions; do
if extension_installed "$extension"; then
continue
Expand Down
12 changes: 6 additions & 6 deletions registry/coder/modules/vscode-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.0"
version = "1.6.1"
agent_id = coder_agent.example.id
accept_license = true
}
Expand All @@ -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.0"
version = "1.6.1"
agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder"
Expand All @@ -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.0"
version = "1.6.1"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
Expand All @@ -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.0"
version = "1.6.1"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
Expand All @@ -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.0"
version = "1.6.1"
agent_id = coder_agent.example.id
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
accept_license = true
Expand All @@ -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.0"
version = "1.6.1"
agent_id = coder_agent.example.id
workspace = "/home/coder/coder.code-workspace"
}
Expand Down
Loading
Loading