Add agent skills to the registry catalogue alongside modules and
templates, following the agentskills.io specification with proper
frontmatter metadata (license, version, author, tags).
- Add registry/coder/skills/coder-modules/ and
registry/coder/skills/coder-templates/ as catalogue entries,
each with SKILL.md (full spec frontmatter + body) and README.md.
- Update .agents/skills/ frontmatter to match catalogue versions.
- Update CI/CD workflows and scripts to support skills:
- deploy-registry.yaml: add skills path trigger
- release.yml: detect skills vs modules for changelog path
- version-bump.yaml: add skills path trigger
- version-bump.sh: include skills in change detection
- tag_release.sh: scan skills directories and extract version
from SKILL.md frontmatter
374 lines
18 KiB
Markdown
374 lines
18 KiB
Markdown
---
|
|
name: coder-modules
|
|
description: Creates and updates Coder Registry modules with proper scaffolding, Terraform testing, README frontmatter, and version management
|
|
license: Apache-2.0
|
|
version: "1.0.0"
|
|
author: coder
|
|
tags: terraform, coder, modules, registry, integrations
|
|
---
|
|
|
|
# Coder Modules
|
|
|
|
Coder Registry modules are reusable Terraform components that live under `registry/<namespace>/modules/<name>/` and are consumed by templates via `module` blocks.
|
|
|
|
## Before You Start
|
|
|
|
Before writing or modifying any code:
|
|
|
|
1. **Understand the request.** What tool, integration, or functionality is the module providing? What Coder resources does it need (`coder_script`, `coder_app`, `coder_env`, etc.)? Read the official documentation for the target tool or integration (installation steps, CLI flags, config files, environment variables, ports) so you can implement the module properly without guessing.
|
|
2. **Research existing modules.** Search the registry for similar modules. Read their `main.tf` to understand patterns, variable conventions, and how they solve similar problems. Avoid duplicating existing functionality.
|
|
3. **Check the Coder provider docs.** Verify that the resources and attributes you plan to use exist in the provider version you're targeting. Use the version-specific docs URL if needed.
|
|
4. **Clarify before building.** If the request is ambiguous (e.g. unclear which Coder resource to use, whether a `coder_app` vs `coder_script` is appropriate, what variables to expose, or which namespace to use), ask for clarification rather than guessing. Never assume a namespace; always confirm with the user.
|
|
5. **Plan the structure.** Decide on script organization (root `run.sh`, `scripts/` directory, or inline), what variables to expose, and what tests to write.
|
|
|
|
Always prefer the proper implementation over a simpler shortcut. Modules are infrastructure that users depend on. Doing less work is not the same as reducing complexity if it leaves the module incomplete or fragile.
|
|
|
|
## Documentation References
|
|
|
|
### Coder
|
|
|
|
- Coder docs (latest): <https://coder.com/docs>
|
|
- Version-specific Coder docs: `https://coder.com/docs/@v{MAJOR}.{MINOR}.{PATCH}` (e.g. <https://coder.com/docs/@v2.31.5>)
|
|
- Coder Registry: <https://registry.coder.com>
|
|
|
|
### Coder Terraform provider
|
|
|
|
- Provider docs (latest): <https://registry.terraform.io/providers/coder/coder/latest/docs>
|
|
- Version-specific provider docs: replace `latest` with a version number (e.g. <https://registry.terraform.io/providers/coder/coder/2.13.1/docs>)
|
|
|
|
Resources:
|
|
|
|
| Resource | Docs |
|
|
| ---------------- | ------------------------------------------------------------------------------------ |
|
|
| `coder_app` | <https://registry.terraform.io/providers/coder/coder/latest/docs/resources/app> |
|
|
| `coder_script` | <https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script> |
|
|
| `coder_env` | <https://registry.terraform.io/providers/coder/coder/latest/docs/resources/env> |
|
|
| `coder_metadata` | <https://registry.terraform.io/providers/coder/coder/latest/docs/resources/metadata> |
|
|
|
|
Data sources:
|
|
|
|
| Data Source | Docs |
|
|
| ----------------------- | ---------------------------------------------------------------------------------------------- |
|
|
| `coder_parameter` | <https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/parameter> |
|
|
| `coder_workspace` | <https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/workspace> |
|
|
| `coder_workspace_owner` | <https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/workspace_owner> |
|
|
|
|
## Scaffolding a New Module
|
|
|
|
Only use this when creating a brand new module that does not yet exist. When updating an existing module, edit its files directly.
|
|
|
|
From repo root:
|
|
|
|
```bash
|
|
./scripts/new_module.sh namespace/module-name
|
|
```
|
|
|
|
Names must be lowercase alphanumeric with hyphens (e.g. `coder/my-tool`). Underscores are not allowed.
|
|
|
|
Creates `registry/<namespace>/modules/<module-name>/` with:
|
|
|
|
- `main.tf`: Terraform config with common resource patterns and variables — read this as the primary reference for module structure
|
|
- `README.md`: frontmatter and usage examples
|
|
- `MODULE_NAME.tftest.hcl`: Terraform native tests
|
|
- `run.sh`: install/start-up script template
|
|
|
|
If the namespace is new, the script also creates `registry/<namespace>/` with a README. New namespaces additionally need:
|
|
|
|
- `registry/<namespace>/.images/avatar.svg` (or `.png`): square image, 400x400px minimum
|
|
- The namespace README `avatar` field pointing to `./.images/avatar.svg`
|
|
|
|
The scaffolding script does not create the `.images/` directory or avatar file. When a new namespace is created, create `registry/<namespace>/.images/` and add a placeholder `avatar.svg` so the directory structure is ready for the user to replace with their real avatar.
|
|
|
|
The generated namespace README contains placeholder fields (`display_name`, `bio`, `status`, `github`, `avatar`, etc.) that the user must fill out. The `status` field is required and must be `official`, `partner`, or `community` (typically `community` for new contributors).
|
|
|
|
## Key Patterns
|
|
|
|
- Provider version constraints must reflect actual functionality requirements. Only raise the minimum `coder` provider version (e.g. `>= 2.5` to `>= 2.8`) when the module uses a resource, attribute, or behavior introduced in that version; check the provider changelog to confirm.
|
|
- Variable names MUST be `snake_case` (no hyphens; validation rejects them)
|
|
- New variables must have sensible defaults for backward compatibility
|
|
- Common variable: `agent_id` (string, required, no default)
|
|
- Common variable: `order` (number, default `null`, controls UI position)
|
|
- Use `locals {}` for computed values: URL normalization, base64 encoding, `file()` script content, config assembly
|
|
- Modules can consume other registry modules via `module` blocks (e.g. `cursor` uses `vscode-desktop-core`, CLI wrappers use `agentapi`). Before consuming a module, read its `main.tf` and `README.md` to understand the full interface: accepted variables, outputs, prerequisites, and runtime requirements. If you are inside the registry repo, read these files directly. Otherwise, read the module's page at `https://registry.coder.com/modules/<namespace>/<module-name>` which includes the full source, README, and variable definitions. Never pass arguments without confirming they exist.
|
|
- Most modules expose configuration via `variable` blocks, letting the template pass values. Use `coder_parameter` inside a module only when the module needs to present a UI choice directly to the workspace user (e.g. region selectors, IDE pickers).
|
|
- For parameter-only modules (region selectors, etc.), use `dynamic "option"` with `for_each` from a `locals` map and expose an `output` for the selected value.
|
|
- `coder_script` icons use the `/icon/<name>.svg` format. The `display_name` is typically the product name (e.g. "code-server", "Git Clone", "File Browser").
|
|
- Do not add comments that narrate what the code does or label sections. Only comment when explaining something non-obvious (e.g. why a workaround exists, a subtle constraint, or an unusual design choice).
|
|
|
|
## README.md
|
|
|
|
Required YAML frontmatter:
|
|
|
|
```yaml
|
|
---
|
|
display_name: My Tool
|
|
description: Short description of what this module does
|
|
icon: ../../../../.icons/tool.svg
|
|
verified: false
|
|
tags: [helper, ide]
|
|
---
|
|
```
|
|
|
|
Content rules:
|
|
|
|
- Single H1 heading matching `display_name`, directly below frontmatter
|
|
- When increasing header levels, increment by one each time (h1 -> h2 -> h3, not h1 -> h3)
|
|
- Usage snippet with `registry.coder.com/<ns>/<module>/coder` and pinned `version`
|
|
- Code fences labeled `tf` (NOT `hcl`)
|
|
- Relative icon paths (e.g. `../../../../.icons/`)
|
|
- **Do NOT include tables or lists that enumerate variables, parameters, or outputs.** The registry generates variable and output documentation automatically from the Terraform source. Describe what the module does and how to use it in prose, not by listing every configurable field.
|
|
- Usage examples are encouraged
|
|
- Use [GFM alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) for callouts: `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, `> [!CAUTION]`
|
|
|
|
```tf
|
|
module "my_tool" {
|
|
count = data.coder_workspace.me.start_count
|
|
source = "registry.coder.com/namespace/my-tool/coder"
|
|
version = "1.0.0"
|
|
agent_id = coder_agent.main.id
|
|
}
|
|
```
|
|
|
|
## Icons
|
|
|
|
Modules reference icons in two places with different path systems:
|
|
|
|
- **README frontmatter** `icon:` uses a relative path to the repo's `.icons/` directory (e.g. `../../../../.icons/my-tool.svg`). Displayed on the registry website.
|
|
- **`coder_script` / `coder_app`** `icon =` uses an absolute `/icon/<name>.svg` path served by the Coder deployment from `site/static/icon/` in the `coder/coder` repo. Displayed in the workspace agent bar.
|
|
|
|
Workflow:
|
|
|
|
1. **Check what exists.** List the `.icons/` directory at the repo root for available SVGs. For `/icon/` paths, look at what similar modules already use.
|
|
2. **Use existing icons when they fit.** If the tool already has an icon in `.icons/` and `/icon/`, use those.
|
|
3. **When an icon doesn't exist,** reference the expected path anyway (e.g. `../../../../.icons/my-tool.svg` and `/icon/my-tool.svg`) so the structure is correct. Try to source the official SVG from the tool's branding page or repository. If you can obtain it, add it to `.icons/` in this repo.
|
|
4. **Don't substitute a generic icon.** If the tool has its own brand identity, use the correct name even if the file doesn't exist yet. Don't fall back to generic icons like `coder.svg` or `terminal.svg`.
|
|
5. **Track missing icons** so you can report them in your response.
|
|
|
|
## Scripts
|
|
|
|
Modules use three patterns for shell logic, depending on complexity:
|
|
|
|
### Root `run.sh` + `templatefile()` (simple modules)
|
|
|
|
A single `run.sh` at the module root, loaded via `templatefile()` to inject Terraform variables. Used by `code-server`, `vscode-web`, `git-clone`, `dotfiles`, `filebrowser`.
|
|
|
|
```tf
|
|
resource "coder_script" "my_tool" {
|
|
agent_id = var.agent_id
|
|
display_name = "My Tool"
|
|
icon = "/icon/my-tool.svg"
|
|
script = templatefile("${path.module}/run.sh", {
|
|
LOG_PATH : var.log_path,
|
|
})
|
|
run_on_start = true
|
|
}
|
|
```
|
|
|
|
Use `$${VAR}` (double dollar) in the shell script for Terraform `templatefile` escaping.
|
|
|
|
If a script sources external files (`$HOME/.bashrc`, `/etc/bashrc`, `/etc/os-release`), the `source` statement must come before `set -u`; CI enforces this ordering.
|
|
|
|
### `scripts/` directory + `file()` (complex modules)
|
|
|
|
Separate `scripts/install.sh` and `scripts/start.sh` loaded via `file()` into `locals`, then passed to a child module or encoded inline. Used by `coder/claude-code`, `coder-labs/copilot`, `coder-labs/codex`, `coder-labs/cursor-cli`, `coder/amazon-q` for example.
|
|
|
|
```tf
|
|
locals {
|
|
install_script = file("${path.module}/scripts/install.sh")
|
|
start_script = file("${path.module}/scripts/start.sh")
|
|
}
|
|
```
|
|
|
|
Use `file()` when scripts don't need Terraform variable interpolation. For config templates, use a `templates/` directory with `templatefile()` (e.g. `coder/amazon-q/templates/agent-config.json.tpl`).
|
|
|
|
### Inline heredoc (minimal modules)
|
|
|
|
For trivial logic, embed the script directly in the `coder_script` resource. Used by `cursor`, `zed`.
|
|
|
|
Modules that use a `scripts/` directory often also have a `testdata/` directory containing mock scripts for testing (e.g. `testdata/my-tool-mock.sh`).
|
|
|
|
## Testing
|
|
|
|
### .tftest.hcl (Required)
|
|
|
|
Every module must have Terraform native tests. The file can be named `main.tftest.hcl` or `<module-name>.tftest.hcl`. Use `command = plan` for most cases:
|
|
|
|
```hcl
|
|
run "plan_with_defaults" {
|
|
command = plan
|
|
|
|
variables {
|
|
agent_id = "test-agent-id"
|
|
}
|
|
|
|
assert {
|
|
condition = var.agent_id == "test-agent-id"
|
|
error_message = "agent_id should be set"
|
|
}
|
|
}
|
|
|
|
run "custom_port" {
|
|
command = plan
|
|
|
|
variables {
|
|
agent_id = "test-agent-id"
|
|
port = 8080
|
|
}
|
|
|
|
assert {
|
|
condition = resource.coder_app.my_tool.url == "http://localhost:8080"
|
|
error_message = "App URL should use configured port"
|
|
}
|
|
}
|
|
```
|
|
|
|
Advanced patterns:
|
|
|
|
- `override_data` to mock data sources like `coder_workspace` and `coder_workspace_owner`
|
|
- `command = apply` when testing outputs or computed values
|
|
- `expect_failures` to test validation rules
|
|
- `regexall()` / `startswith()` / `endswith()` for string assertions
|
|
- Assert on `coder_env`, `coder_script`, `coder_app` resource attributes
|
|
|
|
```hcl
|
|
run "with_mocked_workspace" {
|
|
command = apply
|
|
|
|
variables {
|
|
agent_id = "foo"
|
|
}
|
|
|
|
override_data {
|
|
target = data.coder_workspace.me
|
|
values = {
|
|
name = "test-workspace"
|
|
}
|
|
}
|
|
|
|
assert {
|
|
condition = output.url == "expected-value"
|
|
error_message = "URL should match expected format"
|
|
}
|
|
}
|
|
|
|
run "validation_rejects_conflict" {
|
|
command = plan
|
|
|
|
variables {
|
|
agent_id = "test"
|
|
option_a = true
|
|
option_b = true
|
|
}
|
|
|
|
expect_failures = [
|
|
var.option_a,
|
|
]
|
|
}
|
|
```
|
|
|
|
### main.test.ts (Optional)
|
|
|
|
For more complex testing (Docker containers, script execution, HTTP mocking).
|
|
Import from `~test` (mapped to `test/test.ts` via `tsconfig.json`):
|
|
|
|
```typescript
|
|
import { describe, expect, it } from "bun:test";
|
|
import {
|
|
runTerraformApply,
|
|
runTerraformInit,
|
|
testRequiredVariables,
|
|
findResourceInstance,
|
|
} from "~test";
|
|
|
|
describe("my-tool", () => {
|
|
it("should init successfully", async () => {
|
|
await runTerraformInit(import.meta.dir);
|
|
});
|
|
|
|
testRequiredVariables(import.meta.dir, {
|
|
agent_id: "test-agent",
|
|
});
|
|
|
|
it("should apply with defaults", async () => {
|
|
const state = await runTerraformApply(import.meta.dir, {
|
|
agent_id: "test-agent",
|
|
});
|
|
const app = findResourceInstance(state, "coder_app");
|
|
expect(app.slug).toBe("my-tool");
|
|
expect(app.display_name).toBe("My Tool");
|
|
});
|
|
});
|
|
```
|
|
|
|
### Test utility API (`~test`)
|
|
|
|
**Terraform helpers:**
|
|
|
|
- `runTerraformInit(dir)`: runs `terraform init`.
|
|
- `runTerraformApply(dir, vars, customEnv?)`: runs `terraform apply` with a random state file and returns `TerraformState`. Variables are passed as `TF_VAR_*`. Safe to run in parallel. `TerraformState` has `outputs: Record<string, TerraformOutput>` and `resources: TerraformStateResource[]`.
|
|
- `testRequiredVariables(dir, vars)`: auto-generates test cases (one success with all vars, plus one per var verifying apply fails without it). Pass `{}` if there are no required vars.
|
|
- `findResourceInstance(state, type, name?)`: finds the first resource instance by type. Throws if not found. Optionally filters by name.
|
|
|
|
**Docker helpers** (require `--network host`, Linux/Colima/OrbStack):
|
|
|
|
- `runContainer(image, init?)`: starts a detached container and returns its ID. Labeled `modules-test=true` for auto-cleanup.
|
|
- `removeContainer(id)`: force-removes a container.
|
|
- `execContainer(id, cmd[], args?[])`: runs a command in a container and returns `{ exitCode, stdout, stderr }`.
|
|
- `executeScriptInContainer(state, image, shell?, before?)`: finds `coder_script` in state, runs it in a container, and returns `{ exitCode, stdout: string[], stderr: string[] }`.
|
|
|
|
**File helpers:**
|
|
|
|
- `writeCoder(id, script)`: writes a mock `coder` CLI to `/usr/bin/coder` in the container.
|
|
- `writeFileContainer(id, path, content, { user? })`: writes a file to the container via base64.
|
|
- `readFileContainer(id, path)`: reads a file from the container as root.
|
|
|
|
**HTTP helpers:**
|
|
|
|
- `createJSONResponse(obj, statusCode?)`: creates a `Response` with a JSON body (defaults to 200).
|
|
|
|
Cleanup of `*.tfstate` files and `modules-test` Docker containers is handled automatically by `setup.ts` (preloaded via `bunfig.toml`).
|
|
|
|
## Commands
|
|
|
|
| Task | Command | Scope |
|
|
| ---------------- | ----------------------------------------------------- | ---------- |
|
|
| Format all | `bun run fmt` | Repo |
|
|
| Terraform tests | `bun run tftest` | Repo |
|
|
| TypeScript tests | `bun run tstest` | Repo |
|
|
| Single TF test | `terraform init -upgrade && terraform test -verbose` | Module dir |
|
|
| Single TS test | `bun test main.test.ts` | Module dir |
|
|
| Validate | `./scripts/terraform_validate.sh` | Repo |
|
|
| ShellCheck | `bun run shellcheck` | Repo |
|
|
| Version bump | `.github/scripts/version-bump.sh patch\|minor\|major` | Repo |
|
|
|
|
## Version Management
|
|
|
|
Bump version via `.github/scripts/version-bump.sh` when modifying modules:
|
|
|
|
- `patch`: bugfixes
|
|
- `minor`: new features, new variables with defaults
|
|
- `major`: breaking changes (removed inputs, changed defaults, new required variables)
|
|
|
|
The script automatically updates `version` references in README usage examples.
|
|
|
|
## Final Checks
|
|
|
|
Before considering the work complete, verify:
|
|
|
|
- Tests pass: `bun run tftest` and `bun run tstest`
|
|
- `bun run fmt` has been run
|
|
- `bun run shellcheck` passes if the module includes shell scripts
|
|
- New variables have sensible defaults for backward compatibility
|
|
- Breaking changes are documented if any inputs were removed, defaults changed, or new required variables added
|
|
- Shell scripts handle errors gracefully (`|| echo "Warning..."` for non-fatal failures)
|
|
- No hardcoded values that should be configurable via variables
|
|
- Asset and icon paths in frontmatter and Terraform must be relative (e.g. `../../../../.icons/`), not absolute. External hyperlinks to docs or other websites are fine.
|
|
|
|
## Response to the User
|
|
|
|
In your response, include:
|
|
|
|
- If a new namespace was created, remind the user to fill out the namespace README (`display_name`, `bio`, `status`, `github`, etc.) and replace the placeholder avatar. Note that this is only needed if they plan to contribute to the registry.
|
|
- If any icons were referenced but not found, list them and note they need to be sourced and added to both this repo's `.icons/` directory and the `coder/coder` repo at `site/static/icon/`.
|
|
- A note that to contribute the module to the public registry, they can open a pull request to <https://github.com/coder/registry>.
|