Compare commits

...

5 Commits

Author SHA1 Message Date
blinkagent[bot] eb38bc3092 ci: add variable naming lint to terraform validate (#766)
## Summary

Terraform variable names should use underscores (`snake_case`), not
hyphens. Hyphens are technically valid in HCL but are [deprecated and
non-idiomatic](https://developer.hashicorp.com/terraform/language/values/variables).
This PR adds a variable name check into the existing
`terraform_validate.sh` script so it runs as part of the existing "Run
Terraform Validate" CI step — no new scripts or workflow changes needed.

## Changes

### `scripts/terraform_validate.sh` — added `validate_variable_names()`
- Scans `.tf` files in changed modules for `variable` declarations with
hyphens
- Fails with actionable fix suggestions (shows the snake_case
alternative)
- Runs after `terraform validate` in the same CI step

### Fix: `code-server` module — rename `machine-settings` →
`machine_settings`
- Renames the hyphenated variable and its reference in main.tf
- Bumps version `1.4.2` → `1.4.3`
- Updates all README examples

---
Created on behalf of @matifali

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: DevCats <christofer@coder.com>
2026-02-27 11:15:47 -06:00
Michael Suchacz 93e6094b1b fix: rename add-project to add_project in mux module (#765)
Terraform variable names should use underscores, not hyphens. Renames
the `add-project` variable to `add_project` in the mux module.

**Changes:**
- `main.tf`: Renamed variable declaration and references
- `README.md`: Updated example usage

Bumped version: 1.3.0 → 1.3.1

---
Generated with [Mux](https://mux.coder.com) using Claude
2026-02-27 10:44:39 -06:00
blinkagent[bot] 6ec506e9b6 fix(dotfiles): allow tilde (~) in git repository URLs (#763)
## Description

The URL validation regex in the dotfiles module was rejecting URLs
containing tilde (`~`) characters, which are commonly used in Bitbucket
Server for user repositories (e.g.
`ssh://git@bitbucket.example.org:7999/~username/repo.git`).

This adds `~` to the allowed character set in all three validation
regexes (for `default_dotfiles_uri`, `dotfiles_uri`, and the
`coder_parameter` validation).

## Type of Change

- [ ] New module
- [ ] New template
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder/modules/dotfiles`  
**New version:** `v1.3.1`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [ ] Changes tested locally

## Related Issues

Fixes #762

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2026-02-26 13:30:20 -06:00
Michael Suchacz b794b1edd9 feat(mux): add package_manager and registry_url variables (#761)
## Summary

Add two new customization variables to the Mux module so users can
control how Mux is installed:

### `package_manager` (default: `"auto"`)

Choose which Node package manager installs Mux:

- **`auto`** (default) — auto-detects `npm` → `pnpm` → `bun` in order,
falling back to a direct tarball download when none is available
- **`npm`**, **`pnpm`**, **`bun`** — force a specific package manager
(fails if not found on PATH)

### `registry_url` (default: `"https://registry.npmjs.org"`)

Override the npm registry URL for private registries or mirrors. All
previously hardcoded `registry.npmjs.org` references have been replaced
with this variable. The `--registry` flag is passed to whichever package
manager is used, and the tarball fallback path also uses it.

## Changes

| File | What changed |
|---|---|
| `main.tf` | Added `package_manager` and `registry_url` variables with
validation; pass both to template |
| `run.sh` | Rewrote install logic: PM auto-detection loop,
`case`/`esac` dispatch with PM-specific flags, replaced all hardcoded
registry URLs with `${REGISTRY_URL}` |
| `mux.tftest.hcl` | Added 6 new test cases: PM selection
(npm/pnpm/bun), invalid PM validation, custom registry URL,
trailing-slash stripping |
| `main.test.ts` | Updated expected log messages to match new generic
wording |
| `README.md` | Updated description, added Custom Package Manager and
Custom Registry examples, updated Notes section |

## Version

Bumped **1.2.0 → 1.3.0** (minor: new backward-compatible features).

## Validation

-  `terraform validate` — clean
-  `terraform test` — **15 passed, 0 failed**
-  `terraform fmt` — clean

---

Generated with [Mux](https://mux.coder.com) using Claude
2026-02-26 16:40:40 +01:00
Michael Suchacz 94e41d3780 Add arbitrary mux server command argument parsing (#738)
## Summary
- add a new `additional_arguments` module variable to pass extra
arguments to `mux server`
- parse `additional_arguments` in `run.sh` with quoted-group support so
values like paths with spaces are preserved
- keep existing `add-project` behavior while allowing additional
arbitrary flags
- add Terraform and Bun tests covering `additional_arguments` behavior
- document the new option in the module README and bump example version
references to `1.2.0`

## Why
The module previously only supported the `add-project` flag. This change
lets users pass additional `mux server` arguments without waiting for
new module variables.

## Validation
- `shellcheck --severity=warning --format=gcc
registry/coder/modules/mux/run.sh`
- `terraform -chdir=registry/coder/modules/mux test -verbose`
- `bun test registry/coder/modules/mux/main.test.ts`

## Breaking changes
None.

---
Generated with Mux (exec agent) using GPT-5.
2026-02-25 18:15:33 +00:00
11 changed files with 368 additions and 43 deletions
+8 -8
View File
@@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
module "code-server" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.2" version = "1.4.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -29,7 +29,7 @@ module "code-server" {
module "code-server" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.2" version = "1.4.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
install_version = "4.106.3" install_version = "4.106.3"
} }
@@ -43,7 +43,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
module "code-server" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.2" version = "1.4.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = [ extensions = [
"dracula-theme.theme-dracula" "dracula-theme.theme-dracula"
@@ -61,7 +61,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "code-server" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.2" version = "1.4.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"] extensions = ["dracula-theme.theme-dracula"]
settings = { settings = {
@@ -78,7 +78,7 @@ Just run code-server in the background, don't fetch it from GitHub:
module "code-server" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.2" version = "1.4.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] 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" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.2" version = "1.4.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
additional_args = "--disable-workspace-trust" 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" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.2" version = "1.4.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
use_cached = true use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] 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" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.2" version = "1.4.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
offline = true offline = true
} }
+2 -2
View File
@@ -44,7 +44,7 @@ variable "settings" {
default = {} default = {}
} }
variable "machine-settings" { variable "machine_settings" {
type = any type = any
description = "A map of template level machine settings to apply to code-server. This will be overwritten at each container start." description = "A map of template level machine settings to apply to code-server. This will be overwritten at each container start."
default = {} default = {}
@@ -167,7 +167,7 @@ resource "coder_script" "code-server" {
INSTALL_PREFIX : var.install_prefix, INSTALL_PREFIX : var.install_prefix,
// This is necessary otherwise the quotes are stripped! // This is necessary otherwise the quotes are stripped!
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""), SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
MACHINE_SETTINGS : replace(jsonencode(var.machine-settings), "\"", "\\\""), MACHINE_SETTINGS : replace(jsonencode(var.machine_settings), "\"", "\\\""),
OFFLINE : var.offline, OFFLINE : var.offline,
USE_CACHED : var.use_cached, USE_CACHED : var.use_cached,
USE_CACHED_EXTENSIONS : var.use_cached_extensions, USE_CACHED_EXTENSIONS : var.use_cached_extensions,
+6 -6
View File
@@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -31,7 +31,7 @@ module "dotfiles" {
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -42,7 +42,7 @@ module "dotfiles" {
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
user = "root" user = "root"
} }
@@ -54,14 +54,14 @@ module "dotfiles" {
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
module "dotfiles-root" { module "dotfiles-root" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
user = "root" user = "root"
dotfiles_uri = module.dotfiles.dotfiles_uri dotfiles_uri = module.dotfiles.dotfiles_uri
@@ -76,7 +76,7 @@ You can set a default dotfiles repository for all users by setting the `default_
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles" default_dotfiles_uri = "https://github.com/coder/dotfiles"
} }
@@ -26,6 +26,7 @@ describe("dotfiles", async () => {
"git@github.com:coder/dotfiles.git", "git@github.com:coder/dotfiles.git",
"git://github.com/coder/dotfiles.git", "git://github.com/coder/dotfiles.git",
"ssh://git@github.com/coder/dotfiles.git", "ssh://git@github.com/coder/dotfiles.git",
"ssh://git@bitbucket.example.org:7999/~myusername/dotfiles.git",
]; ];
for (const url of validUrls) { for (const url of validUrls) {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
+3 -3
View File
@@ -40,7 +40,7 @@ variable "default_dotfiles_uri" {
validation { validation {
condition = ( condition = (
var.default_dotfiles_uri == "" || var.default_dotfiles_uri == "" ||
can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$", var.default_dotfiles_uri)) can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@~-]+$", var.default_dotfiles_uri))
) )
error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters."
} }
@@ -55,7 +55,7 @@ variable "dotfiles_uri" {
condition = ( condition = (
var.dotfiles_uri == null || var.dotfiles_uri == null ||
var.dotfiles_uri == "" || var.dotfiles_uri == "" ||
can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$", var.dotfiles_uri)) can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@~-]+$", var.dotfiles_uri))
) )
error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters."
} }
@@ -102,7 +102,7 @@ data "coder_parameter" "dotfiles_uri" {
icon = "/icon/dotfiles.svg" icon = "/icon/dotfiles.svg"
validation { validation {
regex = "^$|^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$" regex = "^$|^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@~-]+$"
error = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." error = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters."
} }
} }
+55 -10
View File
@@ -8,13 +8,13 @@ tags: [ai, agents, development, multiplexer]
# Mux # Mux
Automatically install and run [Mux](https://github.com/coder/mux) in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). Mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces. Automatically install and run [Mux](https://github.com/coder/mux) in a Coder workspace. By default, the module auto-detects an available package manager (`npm`, `pnpm`, or `bun`) to install `mux@next` (with a fallback to downloading the npm tarball if none is found). You can also force a specific package manager via `package_manager` and point to a custom registry with `registry_url`. Mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
```tf ```tf
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.1.0" version = "1.3.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@@ -37,7 +37,7 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.1.0" version = "1.3.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@@ -48,7 +48,7 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.1.0" version = "1.3.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
# Default is "latest"; set to a specific version to pin # Default is "latest"; set to a specific version to pin
install_version = "0.4.0" install_version = "0.4.0"
@@ -63,9 +63,24 @@ Start Mux with `mux server --add-project /path/to/project`:
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.1.0" version = "1.3.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
add-project = "/path/to/project" add_project = "/path/to/project"
}
```
### Pass Arbitrary `mux server` Arguments
Use `additional_arguments` to append additional arguments to `mux server`.
The module parses quoted values, so grouped arguments remain intact.
```tf
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.3.1"
agent_id = coder_agent.main.id
additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'"
} }
``` ```
@@ -75,12 +90,40 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.1.0" version = "1.3.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
port = 8080 port = 8080
} }
``` ```
### Custom Package Manager
Force a specific package manager instead of auto-detection:
```tf
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.3.1"
agent_id = coder_agent.main.id
package_manager = "pnpm" # or "npm", "bun"
}
```
### Custom Registry
Use a private or mirrored npm registry:
```tf
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.3.1"
agent_id = coder_agent.main.id
registry_url = "https://npm.pkg.github.com"
}
```
### Use Cached Installation ### Use Cached Installation
Run an existing copy of Mux if found, otherwise install from npm: Run an existing copy of Mux if found, otherwise install from npm:
@@ -89,7 +132,7 @@ Run an existing copy of Mux if found, otherwise install from npm:
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.1.0" version = "1.3.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
use_cached = true use_cached = true
} }
@@ -103,7 +146,7 @@ Run without installing from the network (requires Mux to be pre-installed):
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.1.0" version = "1.3.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
install = false install = false
} }
@@ -117,4 +160,6 @@ module "mux" {
- Mux is currently in preview and you may encounter bugs - Mux is currently in preview and you may encounter bugs
- Requires internet connectivity for agent operations (unless `install` is set to false) - Requires internet connectivity for agent operations (unless `install` is set to false)
- Installs `mux@next` from npm by default (falls back to the npm tarball if npm is unavailable) - Auto-detects `npm`, `pnpm`, or `bun` by default; set `package_manager` to force a specific one
- Installs `mux@next` from the npm registry by default; set `registry_url` to use a private or mirrored registry
- Falls back to a direct tarball download when no package manager is found
+58 -2
View File
@@ -1,6 +1,11 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import { import {
executeScriptInContainer, executeScriptInContainer,
execContainer,
findResourceInstance,
readFileContainer,
removeContainer,
runContainer,
runTerraformApply, runTerraformApply,
runTerraformInit, runTerraformInit,
testRequiredVariables, testRequiredVariables,
@@ -30,7 +35,7 @@ describe("mux", async () => {
} }
expect(output.exitCode).toBe(0); expect(output.exitCode).toBe(0);
const expectedLines = [ const expectedLines = [
"📥 npm not found; downloading tarball from npm registry...", "📥 No package manager found; downloading tarball from registry...",
"🥳 mux has been installed in /tmp/mux", "🥳 mux has been installed in /tmp/mux",
"🚀 Starting mux server on port 4000...", "🚀 Starting mux server on port 4000...",
"Check logs at /tmp/mux.log!", "Check logs at /tmp/mux.log!",
@@ -40,6 +45,57 @@ describe("mux", async () => {
} }
}, 60000); }, 60000);
it("parses custom additional_arguments", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
install: false,
log_path: "/tmp/mux.log",
additional_arguments:
"--open-mode pinned --add-project '/workspaces/my repo'",
});
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer("alpine/curl");
try {
const setup = await execContainer(id, [
"sh",
"-c",
`apk add --no-cache bash >/dev/null
mkdir -p /tmp/mux
cat <<'EOF' > /tmp/mux/mux
#!/usr/bin/env sh
i=1
for arg in "$@"; do
echo "arg$i=$arg"
i=$((i + 1))
done
EOF
chmod +x /tmp/mux/mux`,
]);
expect(setup.exitCode).toBe(0);
const output = await execContainer(id, ["sh", "-c", instance.script]);
if (output.exitCode !== 0) {
console.log("STDOUT:\n" + output.stdout);
console.log("STDERR:\n" + output.stderr);
}
expect(output.exitCode).toBe(0);
await execContainer(id, ["sh", "-c", "sleep 1"]);
const log = await readFileContainer(id, "/tmp/mux.log");
expect(log).toContain("arg1=server");
expect(log).toContain("arg2=--port");
expect(log).toContain("arg3=4000");
expect(log).toContain("arg4=--open-mode");
expect(log).toContain("arg5=pinned");
expect(log).toContain("arg6=--add-project");
expect(log).toContain("arg7=/workspaces/my repo");
} finally {
await removeContainer(id);
}
}, 60000);
it("runs with npm present", async () => { it("runs with npm present", async () => {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
@@ -55,7 +111,7 @@ describe("mux", async () => {
expect(output.exitCode).toBe(0); expect(output.exitCode).toBe(0);
const expectedLines = [ const expectedLines = [
"📦 Installing mux via npm into /tmp/mux...", "📦 Installing mux via npm into /tmp/mux...",
"⏭️ Skipping npm lifecycle scripts with --ignore-scripts", "⏭️ Skipping lifecycle scripts with --ignore-scripts",
"🥳 mux has been installed in /tmp/mux", "🥳 mux has been installed in /tmp/mux",
"🚀 Starting mux server on port 4000...", "🚀 Starting mux server on port 4000...",
"Check logs at /tmp/mux.log!", "Check logs at /tmp/mux.log!",
+29 -2
View File
@@ -49,18 +49,41 @@ variable "log_path" {
default = "/tmp/mux.log" default = "/tmp/mux.log"
} }
variable "add-project" { variable "add_project" {
type = string type = string
description = "Optional path to add/open as a project in Mux on startup." description = "Optional path to add/open as a project in Mux on startup."
default = null default = null
} }
variable "additional_arguments" {
type = string
description = "Additional command-line arguments to pass to `mux server` (for example: `--add-project /path --open-mode pinned`)."
default = ""
}
variable "install_version" { variable "install_version" {
type = string type = string
description = "The version or dist-tag of Mux to install." description = "The version or dist-tag of Mux to install."
default = "next" default = "next"
} }
variable "package_manager" {
type = string
description = "Package manager to install Mux. 'auto' detects npm, pnpm, or bun (falling back to tarball download). Set to 'npm', 'pnpm', or 'bun' to force a specific one."
default = "auto"
validation {
condition = contains(["auto", "npm", "pnpm", "bun"], var.package_manager)
error_message = "The 'package_manager' variable must be one of: 'auto', 'npm', 'pnpm', 'bun'."
}
}
variable "registry_url" {
type = string
description = "The npm-compatible registry URL to install Mux from. Override this for private registries or mirrors."
default = "https://registry.npmjs.org"
}
variable "share" { variable "share" {
type = string type = string
default = "owner" default = "owner"
@@ -131,6 +154,7 @@ resource "random_password" "mux_auth_token" {
locals { locals {
mux_auth_token = random_password.mux_auth_token.result mux_auth_token = random_password.mux_auth_token.result
registry_url = trimsuffix(var.registry_url, "/")
} }
resource "coder_script" "mux" { resource "coder_script" "mux" {
@@ -141,11 +165,14 @@ resource "coder_script" "mux" {
VERSION : var.install_version, VERSION : var.install_version,
PORT : var.port, PORT : var.port,
LOG_PATH : var.log_path, LOG_PATH : var.log_path,
ADD_PROJECT : var.add-project == null ? "" : var.add-project, ADD_PROJECT : var.add_project == null ? "" : var.add_project,
ADDITIONAL_ARGUMENTS : var.additional_arguments,
INSTALL_PREFIX : var.install_prefix, INSTALL_PREFIX : var.install_prefix,
OFFLINE : !var.install, OFFLINE : !var.install,
USE_CACHED : var.use_cached, USE_CACHED : var.use_cached,
AUTH_TOKEN : local.mux_auth_token, AUTH_TOKEN : local.mux_auth_token,
PACKAGE_MANAGER : var.package_manager,
REGISTRY_URL : local.registry_url,
}) })
run_on_start = true run_on_start = true
+107
View File
@@ -79,6 +79,20 @@ run "auth_token_in_url" {
} }
} }
run "custom_additional_arguments" {
command = plan
variables {
agent_id = "foo"
additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "--open-mode pinned --add-project '/workspaces/my repo'")
error_message = "mux launch script must include the configured additional arguments"
}
}
run "custom_version" { run "custom_version" {
command = plan command = plan
@@ -107,3 +121,96 @@ run "use_cached_only_success" {
use_cached = true use_cached = true
} }
} }
# Custom package_manager should appear in generated script
run "custom_package_manager_npm" {
command = plan
variables {
agent_id = "foo"
package_manager = "npm"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "PM_CMD=\"npm\"")
error_message = "mux script must set PM_CMD to the configured package manager"
}
}
run "custom_package_manager_pnpm" {
command = plan
variables {
agent_id = "foo"
package_manager = "pnpm"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "PM_CMD=\"pnpm\"")
error_message = "mux script must set PM_CMD to the configured package manager"
}
}
run "custom_package_manager_bun" {
command = plan
variables {
agent_id = "foo"
package_manager = "bun"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "PM_CMD=\"bun\"")
error_message = "mux script must set PM_CMD to the configured package manager"
}
}
# Invalid package_manager should fail validation
run "invalid_package_manager" {
command = plan
variables {
agent_id = "foo"
package_manager = "yarn"
}
expect_failures = [
var.package_manager
]
}
# Custom registry_url should appear in generated script
run "custom_registry_url" {
command = plan
variables {
agent_id = "foo"
registry_url = "https://npm.example.com"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "https://npm.example.com")
error_message = "mux script must use the configured registry URL"
}
assert {
condition = !strcontains(resource.coder_script.mux.script, "registry.npmjs.org")
error_message = "mux script must not contain hardcoded registry.npmjs.org when custom registry is set"
}
}
# registry_url trailing slash should be stripped
run "registry_url_trailing_slash" {
command = plan
variables {
agent_id = "foo"
registry_url = "https://npm.example.com/"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "https://npm.example.com/mux/")
error_message = "registry URL trailing slash must be stripped to avoid double slashes"
}
}
+61 -10
View File
@@ -20,6 +20,22 @@ function run_mux() {
if [ -n "${ADD_PROJECT}" ]; then if [ -n "${ADD_PROJECT}" ]; then
set -- "$@" --add-project "${ADD_PROJECT}" set -- "$@" --add-project "${ADD_PROJECT}"
fi fi
# Parse additional user-supplied server arguments while preserving quoted groups.
if [ -n "${ADDITIONAL_ARGUMENTS}" ]; then
local parsed_additional_arguments
if ! parsed_additional_arguments="$(printf "%s\n" "${ADDITIONAL_ARGUMENTS}" | xargs -n1 printf "%s\n" 2> /dev/null)"; then
echo "❌ Failed to parse additional_arguments. Ensure quotes are balanced."
exit 1
fi
while IFS= read -r parsed_arg; do
[ -n "$parsed_arg" ] || continue
set -- "$@" "$parsed_arg"
done << EOF
$${parsed_additional_arguments}
EOF
fi
echo "🚀 Starting mux server on port $port_value..." echo "🚀 Starting mux server on port $port_value..."
echo "Check logs at ${LOG_PATH}!" echo "Check logs at ${LOG_PATH}!"
MUX_SERVER_AUTH_TOKEN="$auth_token_value" PORT="$port_value" "$MUX_BINARY" "$@" > "${LOG_PATH}" 2>&1 & MUX_SERVER_AUTH_TOKEN="$auth_token_value" PORT="$port_value" "$MUX_BINARY" "$@" > "${LOG_PATH}" 2>&1 &
@@ -38,7 +54,7 @@ fi
# If there is no cached install OR we don't want to use a cached install # If there is no cached install OR we don't want to use a cached install
if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
printf "$${BOLD}Installing mux from npm...\n" printf "$${BOLD}Installing mux...\n"
# Clean up from other install (in case install prefix changed). # Clean up from other install (in case install prefix changed).
if [ -n "$CODER_SCRIPT_BIN_DIR" ] && [ -e "$CODER_SCRIPT_BIN_DIR/mux" ]; then if [ -n "$CODER_SCRIPT_BIN_DIR" ] && [ -e "$CODER_SCRIPT_BIN_DIR/mux" ]; then
@@ -47,41 +63,76 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
mkdir -p "$(dirname "$MUX_BINARY")" mkdir -p "$(dirname "$MUX_BINARY")"
if command -v npm > /dev/null 2>&1; then # Determine which package manager to use
echo "📦 Installing mux via npm into ${INSTALL_PREFIX}..." PM_CMD=""
if [ "${PACKAGE_MANAGER}" = "auto" ]; then
for pm in npm pnpm bun; do
if command -v "$pm" > /dev/null 2>&1; then
PM_CMD="$pm"
break
fi
done
else
PM_CMD="${PACKAGE_MANAGER}"
if ! command -v "$PM_CMD" > /dev/null 2>&1; then
echo "❌ Configured package manager '${PACKAGE_MANAGER}' not found on PATH"
exit 1
fi
fi
if [ -n "$PM_CMD" ]; then
echo "📦 Installing mux via $PM_CMD into ${INSTALL_PREFIX}..."
NPM_WORKDIR="${INSTALL_PREFIX}/npm" NPM_WORKDIR="${INSTALL_PREFIX}/npm"
mkdir -p "$NPM_WORKDIR" mkdir -p "$NPM_WORKDIR"
cd "$NPM_WORKDIR" || exit 1 cd "$NPM_WORKDIR" || exit 1
if [ ! -f package.json ]; then if [ ! -f package.json ]; then
echo '{}' > package.json echo '{}' > package.json
fi fi
echo "⏭️ Skipping npm lifecycle scripts with --ignore-scripts" echo "⏭️ Skipping lifecycle scripts with --ignore-scripts"
PKG="mux" PKG="mux"
if [ -z "${VERSION}" ] || [ "${VERSION}" = "latest" ]; then if [ -z "${VERSION}" ] || [ "${VERSION}" = "latest" ]; then
PKG_SPEC="$PKG@latest" PKG_SPEC="$PKG@latest"
else else
PKG_SPEC="$PKG@${VERSION}" PKG_SPEC="$PKG@${VERSION}"
fi fi
if ! npm install --no-audit --no-fund --omit=dev --ignore-scripts "$PKG_SPEC"; then INSTALL_OK=true
echo "❌ Failed to install mux via npm" case "$PM_CMD" in
npm)
if ! npm install --no-audit --no-fund --omit=dev --ignore-scripts --registry "${REGISTRY_URL}" "$PKG_SPEC"; then
INSTALL_OK=false
fi
;;
pnpm)
if ! pnpm add --ignore-scripts --registry "${REGISTRY_URL}" "$PKG_SPEC"; then
INSTALL_OK=false
fi
;;
bun)
if ! bun add --ignore-scripts --registry "${REGISTRY_URL}" "$PKG_SPEC"; then
INSTALL_OK=false
fi
;;
esac
if [ "$INSTALL_OK" != true ]; then
echo "❌ Failed to install mux via $PM_CMD"
exit 1 exit 1
fi fi
# Determine the installed binary path # Determine the installed binary path
BIN_DIR="$NPM_WORKDIR/node_modules/.bin" BIN_DIR="$NPM_WORKDIR/node_modules/.bin"
CANDIDATE="$BIN_DIR/mux" CANDIDATE="$BIN_DIR/mux"
if [ ! -f "$CANDIDATE" ]; then if [ ! -f "$CANDIDATE" ]; then
echo "❌ Could not locate mux binary after npm install" echo "❌ Could not locate mux binary after $PM_CMD install"
exit 1 exit 1
fi fi
chmod +x "$CANDIDATE" || true chmod +x "$CANDIDATE" || true
ln -sf "$CANDIDATE" "$MUX_BINARY" ln -sf "$CANDIDATE" "$MUX_BINARY"
else else
echo "📥 npm not found; downloading tarball from npm registry..." echo "📥 No package manager found; downloading tarball from registry..."
VERSION_TO_USE="${VERSION}" VERSION_TO_USE="${VERSION}"
if [ -z "$VERSION_TO_USE" ]; then if [ -z "$VERSION_TO_USE" ]; then
VERSION_TO_USE="next" VERSION_TO_USE="next"
fi fi
META_URL="https://registry.npmjs.org/mux/$VERSION_TO_USE" META_URL="${REGISTRY_URL}/mux/$VERSION_TO_USE"
META_JSON="$(curl -fsSL "$META_URL" || true)" META_JSON="$(curl -fsSL "$META_URL" || true)"
if [ -z "$META_JSON" ]; then if [ -z "$META_JSON" ]; then
echo "❌ Failed to fetch npm metadata: $META_URL" echo "❌ Failed to fetch npm metadata: $META_URL"
@@ -120,7 +171,7 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
echo "❌ Could not determine version for mux" echo "❌ Could not determine version for mux"
exit 1 exit 1
fi fi
TARBALL_URL="https://registry.npmjs.org/mux/-/mux-$VERSION_TO_USE.tgz" TARBALL_URL="${REGISTRY_URL}/mux/-/mux-$VERSION_TO_USE.tgz"
fi fi
TMP_DIR="$(mktemp -d)" TMP_DIR="$(mktemp -d)"
TAR_PATH="$TMP_DIR/mux.tgz" TAR_PATH="$TMP_DIR/mux.tgz"
+38
View File
@@ -11,6 +11,34 @@ set -euo pipefail
# #
# This script only validates changed modules. Documentation and template changes are ignored. # This script only validates changed modules. Documentation and template changes are ignored.
# Validates that Terraform variable names use underscores (snake_case) instead
# of hyphens. Hyphens are technically valid but deprecated and non-idiomatic.
# See: https://developer.hashicorp.com/terraform/language/values/variables
validate_variable_names() {
local dir="$1"
local found_issues=0
while IFS= read -r tf_file; do
while IFS= read -r match; do
local line_num
line_num=$(echo "$match" | cut -d: -f1)
local line_content
line_content=$(echo "$match" | cut -d: -f2-)
local var_name
var_name=$(echo "$line_content" | sed -n 's/.*variable "\([^"]*\)".*/\1/p')
if [[ -n "$var_name" ]]; then
echo " ERROR: $tf_file:$line_num"
echo " Variable \"$var_name\" contains a hyphen."
echo " Rename to \"${var_name//-/_}\" (use underscores instead of hyphens)."
found_issues=$((found_issues + 1))
fi
done < <(grep -n 'variable "[^"]*-[^"]*"' "$tf_file" 2> /dev/null || true)
done < <(find "$dir" -name '*.tf' -type f | sort)
return "$found_issues"
}
validate_terraform_directory() { validate_terraform_directory() {
local dir="$1" local dir="$1"
echo "Running \`terraform validate\` in $dir" echo "Running \`terraform validate\` in $dir"
@@ -91,6 +119,16 @@ main() {
fi fi
done done
echo ""
echo "==> Validating Terraform variable names use snake_case..."
for dir in $subdirs; do
if test -f "$dir/main.tf"; then
if ! validate_variable_names "$dir"; then
status=1
fi
fi
done
exit $status exit $status
} }