Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The `freeform` template is unique: it keeps ddev-router running so multiple DDEV
## Tool Preferences

- Use `jq` (not `python3 -m json.tool`) for JSON pretty-printing and querying
- For `git commit -m` and `gh pr create --body`, write the message to a file first (e.g. under the scratchpad directory) and pass it via `git commit -F <file>` / `gh pr create --body-file <file>`, rather than inlining a heredoc on the command line. Multi-paragraph messages containing backticks, apostrophes, or other shell-special characters reliably break inline heredoc quoting.

## Before Pushing / Pre-push Checklist

Expand All @@ -59,19 +60,23 @@ Run these before every push to avoid CI failures:
# Terraform formatting (CI runs terraform fmt -check -recursive)
terraform fmt -recursive

# Terraform validation for each template you touched
terraform -chdir=drupal-core init -backend=false && terraform -chdir=drupal-core validate
terraform -chdir=drupal-contrib init -backend=false && terraform -chdir=drupal-contrib validate
terraform -chdir=freeform init -backend=false && terraform -chdir=freeform validate

# Terraform tests (plan-level, no real infrastructure)
terraform -chdir=drupal-core test
terraform -chdir=drupal-contrib test
terraform -chdir=freeform test
# Vendor shared assets, then validate + test every template
make validate
make test-templates
```

`terraform fmt -recursive` must be run from the repo root. It is non-destructive (rewrites in place) and the CI check fails with exit code 3 if any file is not formatted.

**Always use `make validate` / `make test-templates`, not raw `terraform validate`/`terraform test` run directly in a template directory.** Both targets depend on `make sync-shared`, which vendors `modules/claude-remote-control` and `shared/vscode-extensions.tf` into each template directory first (see "Shared Terraform Assets" below). Running `terraform validate`/`test` directly against a template dir will happily validate against a stale vendored copy and miss the fact that it's out of sync with the canonical source.

## Shared Terraform Assets

Some Terraform config (currently `modules/claude-remote-control` and `shared/vscode-extensions.tf`) is shared across all three templates but must exist as a **physical copy inside each template directory** — `coder templates push` and CI's `terraform validate`/`test` only ever operate on a single template directory, so a relative `../modules` or `../shared` reference would not survive the push and would not be visible to CI.

- **Edit only the canonical source**: `modules/claude-remote-control/` or `shared/vscode-extensions.tf` at the repo root. Never hand-edit the vendored copies (`<template>/modules/claude-remote-control/`, `<template>/vscode-extensions.tf`) — they get overwritten the next time anyone runs the sync.
- **Always resync through `make`**, never by hand-copying: `make sync-shared` (or `make sync-claude-module` / `make sync-vscode-extensions` individually). `make validate`, `make test-templates`, and every `make push-template-*` / `make push-all-templates` target already depend on `sync-shared`, so day-to-day you rarely need to invoke it directly.
- **Commit the vendored copies.** They are tracked files, not build artifacts — CI validates each template directory standalone with no `make` step, so the vendored copies must already be correct and committed for CI to pass.

## Working with Coder Workspaces via SSH

After running `coder config-ssh --yes`, workspaces are available as SSH hosts named `<workspace>.coder`. Use `scp` to copy files in or out, then `ssh` to execute scripts non-interactively:
Expand Down
20 changes: 15 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,18 @@ sync-claude-module: ## Vendor modules/claude-remote-control into each template d
done
@echo "Synced modules/claude-remote-control into: $(TEMPLATES)"

.PHONY: sync-vscode-extensions
sync-vscode-extensions: ## Vendor shared/vscode-extensions.tf into each template dir (same reason as sync-claude-module: push only bundles the given --directory)
@for t in $(TEMPLATES); do \
cp shared/vscode-extensions.tf $$t/vscode-extensions.tf; \
done
@echo "Synced shared/vscode-extensions.tf into: $(TEMPLATES)"

.PHONY: sync-shared
sync-shared: sync-claude-module sync-vscode-extensions ## Vendor all shared Terraform assets (modules + vscode-extensions.tf) into each template dir

.PHONY: validate
validate: sync-claude-module ## Validate all Terraform templates (requires terraform in PATH)
validate: sync-shared ## Validate all Terraform templates (requires terraform in PATH)
@for t in $(TEMPLATES); do \
echo "--- Validating $$t ---"; \
(cd $$t && terraform init -backend=false -input=false -no-color && terraform validate -no-color) || exit 1; \
Expand All @@ -125,7 +135,7 @@ fmt-check: ## Check Terraform formatting across all templates
terraform fmt -check -recursive

.PHONY: test-templates
test-templates: sync-claude-module ## Run Terraform mock unit tests for all templates (requires terraform in PATH)
test-templates: sync-shared ## Run Terraform mock unit tests for all templates (requires terraform in PATH)
@for t in $(TEMPLATES); do \
echo "--- Testing $$t ---"; \
(cd $$t && terraform test) || exit 1; \
Expand Down Expand Up @@ -170,15 +180,15 @@ info: ## Show image and template information
# --- Template push targets ---

.PHONY: push-template-drupal-core
push-template-drupal-core: sync-claude-module ## Push drupal-core template to Coder
push-template-drupal-core: sync-shared ## Push drupal-core template to Coder
$(call push_template,drupal-core)

.PHONY: push-template-drupal-contrib
push-template-drupal-contrib: sync-claude-module ## Push drupal-contrib template to Coder
push-template-drupal-contrib: sync-shared ## Push drupal-contrib template to Coder
$(call push_template,drupal-contrib)

.PHONY: push-template-freeform
push-template-freeform: sync-claude-module ## Push freeform template to Coder
push-template-freeform: sync-shared ## Push freeform template to Coder
$(call push_template,freeform)

.PHONY: push-all-templates
Expand Down
29 changes: 2 additions & 27 deletions drupal-contrib/template.tf
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,12 @@ data "coder_parameter" "vscode_extensions" {
description = "Select extensions to enable in VS Code for Web"
type = "list(string)"
form_type = "multi-select"
default = jsonencode([for e in var.vscode_extensions : e.id if e.default])
default = jsonencode([for e in local.vscode_extensions : e.id if e.default])
mutable = true
order = 100

dynamic "option" {
for_each = var.vscode_extensions
for_each = local.vscode_extensions
content {
name = option.value.name
value = option.value.id
Expand Down Expand Up @@ -224,31 +224,6 @@ locals {
workspace_image_registry_base = replace(local.registry_without_version, ":latest", "")
}

variable "vscode_extensions" {
description = "List of VS Code extensions to offer in the workspace creation UI"
type = list(object({
id = string
name = string
default = bool
}))
default = [
{ id = "xdebug.php-debug", name = "PHP Debug", default = true },
{ id = "bmewburn.vscode-intelephense-client", name = "Intelephense", default = true },
{ id = "dbaeumer.vscode-eslint", name = "ESLint", default = true },
{ id = "esbenp.prettier-vscode", name = "Prettier", default = true },
{ id = "sanderronde.phpstan-vscode", name = "PHPStan", default = true },
{ id = "streetsidesoftware.code-spell-checker", name = "Code Spell Checker", default = true },
{ id = "stylelint.vscode-stylelint", name = "Stylelint", default = true },
{ id = "valeryanm.vscode-phpsab", name = "PHPSAB", default = true },
{ id = "biati.ddev-manager", name = "DDEV Manager", default = true },
{ id = "deque-systems.vscode-axe-linter", name = "Axe Linter", default = false },
{ id = "andrewdavidblum.drupal-smart-snippets", name = "Drupal Smart Snippets", default = false },
{ id = "redhat.vscode-yaml", name = "YAML", default = false },
{ id = "sleistner.vscode-fileutils", name = "File Utils", default = false },
{ id = "GitHub.vscode-pull-request-github", name = "GitHub Pull Requests", default = false },
]
}

variable "workspace_image_registry" {
description = "Docker registry URL for the workspace base image (without tag, version is added automatically)"
type = string
Expand Down
24 changes: 24 additions & 0 deletions drupal-contrib/vscode-extensions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
locals {
# Deliberately a `locals`, not a `variable`: Coder pins the submitted value of any
# root-level `variable` block server-side the first time a template is pushed, and
# reuses that pinned value on later pushes instead of re-reading this file's list
# unless `--variable vscode_extensions=...` is passed again. `locals` are recomputed
# fresh on every plan, so there is nothing for Coder to pin.
vscode_extensions = [
{ id = "xdebug.php-debug", name = "PHP Debug", default = true },
{ id = "bmewburn.vscode-intelephense-client", name = "Intelephense", default = true },
{ id = "dbaeumer.vscode-eslint", name = "ESLint", default = true },
{ id = "esbenp.prettier-vscode", name = "Prettier", default = true },
{ id = "sanderronde.phpstan-vscode", name = "PHPStan", default = true },
{ id = "streetsidesoftware.code-spell-checker", name = "Code Spell Checker", default = true },
{ id = "stylelint.vscode-stylelint", name = "Stylelint", default = true },
{ id = "valeryanm.vscode-phpsab", name = "PHPSAB", default = true },
{ id = "biati.ddev-manager", name = "DDEV Manager", default = true },
{ id = "golang.go", name = "Go", default = true },
{ id = "deque-systems.vscode-axe-linter", name = "Axe Linter", default = false },
{ id = "andrewdavidblum.drupal-smart-snippets", name = "Drupal Smart Snippets", default = false },
{ id = "redhat.vscode-yaml", name = "YAML", default = false },
{ id = "sleistner.vscode-fileutils", name = "File Utils", default = false },
{ id = "GitHub.vscode-pull-request-github", name = "GitHub Pull Requests", default = false },
]
}
29 changes: 2 additions & 27 deletions drupal-core/template.tf
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,12 @@ data "coder_parameter" "vscode_extensions" {
description = "Select extensions to enable in VS Code for Web"
type = "list(string)"
form_type = "multi-select"
default = jsonencode([for e in var.vscode_extensions : e.id if e.default])
default = jsonencode([for e in local.vscode_extensions : e.id if e.default])
mutable = true
order = 100

dynamic "option" {
for_each = var.vscode_extensions
for_each = local.vscode_extensions
content {
name = option.value.name
value = option.value.id
Expand Down Expand Up @@ -224,31 +224,6 @@ locals {
workspace_image_registry_base = replace(local.registry_without_version, ":latest", "")
}

variable "vscode_extensions" {
description = "List of VS Code extensions to offer in the workspace creation UI"
type = list(object({
id = string
name = string
default = bool
}))
default = [
{ id = "xdebug.php-debug", name = "PHP Debug", default = true },
{ id = "bmewburn.vscode-intelephense-client", name = "Intelephense", default = true },
{ id = "dbaeumer.vscode-eslint", name = "ESLint", default = true },
{ id = "esbenp.prettier-vscode", name = "Prettier", default = true },
{ id = "sanderronde.phpstan-vscode", name = "PHPStan", default = true },
{ id = "streetsidesoftware.code-spell-checker", name = "Code Spell Checker", default = true },
{ id = "stylelint.vscode-stylelint", name = "Stylelint", default = true },
{ id = "valeryanm.vscode-phpsab", name = "PHPSAB", default = true },
{ id = "biati.ddev-manager", name = "DDEV Manager", default = true },
{ id = "deque-systems.vscode-axe-linter", name = "Axe Linter", default = false },
{ id = "andrewdavidblum.drupal-smart-snippets", name = "Drupal Smart Snippets", default = false },
{ id = "redhat.vscode-yaml", name = "YAML", default = false },
{ id = "sleistner.vscode-fileutils", name = "File Utils", default = false },
{ id = "GitHub.vscode-pull-request-github", name = "GitHub Pull Requests", default = false },
]
}

variable "workspace_image_registry" {
description = "Docker registry URL for the workspace base image (without tag, version is added automatically)"
type = string
Expand Down
24 changes: 24 additions & 0 deletions drupal-core/vscode-extensions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
locals {
# Deliberately a `locals`, not a `variable`: Coder pins the submitted value of any
# root-level `variable` block server-side the first time a template is pushed, and
# reuses that pinned value on later pushes instead of re-reading this file's list
# unless `--variable vscode_extensions=...` is passed again. `locals` are recomputed
# fresh on every plan, so there is nothing for Coder to pin.
vscode_extensions = [
{ id = "xdebug.php-debug", name = "PHP Debug", default = true },
{ id = "bmewburn.vscode-intelephense-client", name = "Intelephense", default = true },
{ id = "dbaeumer.vscode-eslint", name = "ESLint", default = true },
{ id = "esbenp.prettier-vscode", name = "Prettier", default = true },
{ id = "sanderronde.phpstan-vscode", name = "PHPStan", default = true },
{ id = "streetsidesoftware.code-spell-checker", name = "Code Spell Checker", default = true },
{ id = "stylelint.vscode-stylelint", name = "Stylelint", default = true },
{ id = "valeryanm.vscode-phpsab", name = "PHPSAB", default = true },
{ id = "biati.ddev-manager", name = "DDEV Manager", default = true },
{ id = "golang.go", name = "Go", default = true },
{ id = "deque-systems.vscode-axe-linter", name = "Axe Linter", default = false },
{ id = "andrewdavidblum.drupal-smart-snippets", name = "Drupal Smart Snippets", default = false },
{ id = "redhat.vscode-yaml", name = "YAML", default = false },
{ id = "sleistner.vscode-fileutils", name = "File Utils", default = false },
{ id = "GitHub.vscode-pull-request-github", name = "GitHub Pull Requests", default = false },
]
}
29 changes: 2 additions & 27 deletions freeform/template.tf
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ data "coder_parameter" "vscode_extensions" {
description = "Select extensions to enable in VS Code for Web"
type = "list(string)"
form_type = "multi-select"
default = jsonencode([for e in var.vscode_extensions : e.id if e.default])
default = jsonencode([for e in local.vscode_extensions : e.id if e.default])
mutable = true
order = 100

dynamic "option" {
for_each = var.vscode_extensions
for_each = local.vscode_extensions
content {
name = option.value.name
value = option.value.id
Expand All @@ -113,31 +113,6 @@ locals {
)
}

variable "vscode_extensions" {
description = "List of VS Code extensions to offer in the workspace creation UI"
type = list(object({
id = string
name = string
default = bool
}))
default = [
{ id = "xdebug.php-debug", name = "PHP Debug", default = true },
{ id = "bmewburn.vscode-intelephense-client", name = "Intelephense", default = true },
{ id = "dbaeumer.vscode-eslint", name = "ESLint", default = true },
{ id = "esbenp.prettier-vscode", name = "Prettier", default = true },
{ id = "sanderronde.phpstan-vscode", name = "PHPStan", default = true },
{ id = "streetsidesoftware.code-spell-checker", name = "Code Spell Checker", default = true },
{ id = "stylelint.vscode-stylelint", name = "Stylelint", default = true },
{ id = "valeryanm.vscode-phpsab", name = "PHPSAB", default = true },
{ id = "biati.ddev-manager", name = "DDEV Manager", default = true },
{ id = "deque-systems.vscode-axe-linter", name = "Axe Linter", default = false },
{ id = "andrewdavidblum.drupal-smart-snippets", name = "Drupal Smart Snippets", default = false },
{ id = "redhat.vscode-yaml", name = "YAML", default = false },
{ id = "sleistner.vscode-fileutils", name = "File Utils", default = false },
{ id = "GitHub.vscode-pull-request-github", name = "GitHub Pull Requests", default = false },
]
}

variable "workspace_image_registry" {
description = "Docker registry URL for the workspace base image (without tag)"
type = string
Expand Down
24 changes: 24 additions & 0 deletions freeform/vscode-extensions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
locals {
# Deliberately a `locals`, not a `variable`: Coder pins the submitted value of any
# root-level `variable` block server-side the first time a template is pushed, and
# reuses that pinned value on later pushes instead of re-reading this file's list
# unless `--variable vscode_extensions=...` is passed again. `locals` are recomputed
# fresh on every plan, so there is nothing for Coder to pin.
vscode_extensions = [
{ id = "xdebug.php-debug", name = "PHP Debug", default = true },
{ id = "bmewburn.vscode-intelephense-client", name = "Intelephense", default = true },
{ id = "dbaeumer.vscode-eslint", name = "ESLint", default = true },
{ id = "esbenp.prettier-vscode", name = "Prettier", default = true },
{ id = "sanderronde.phpstan-vscode", name = "PHPStan", default = true },
{ id = "streetsidesoftware.code-spell-checker", name = "Code Spell Checker", default = true },
{ id = "stylelint.vscode-stylelint", name = "Stylelint", default = true },
{ id = "valeryanm.vscode-phpsab", name = "PHPSAB", default = true },
{ id = "biati.ddev-manager", name = "DDEV Manager", default = true },
{ id = "golang.go", name = "Go", default = true },
{ id = "deque-systems.vscode-axe-linter", name = "Axe Linter", default = false },
{ id = "andrewdavidblum.drupal-smart-snippets", name = "Drupal Smart Snippets", default = false },
{ id = "redhat.vscode-yaml", name = "YAML", default = false },
{ id = "sleistner.vscode-fileutils", name = "File Utils", default = false },
{ id = "GitHub.vscode-pull-request-github", name = "GitHub Pull Requests", default = false },
]
}
1 change: 1 addition & 0 deletions image/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ RUN mkdir -p "$HOMEBREW_CACHE" && /home/linuxbrew/.linuxbrew/bin/brew install \
claude-code \
go \
golangci-lint \
gopls \
yq \
zellij

Expand Down
24 changes: 24 additions & 0 deletions shared/vscode-extensions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
locals {
# Deliberately a `locals`, not a `variable`: Coder pins the submitted value of any
# root-level `variable` block server-side the first time a template is pushed, and
# reuses that pinned value on later pushes instead of re-reading this file's list
# unless `--variable vscode_extensions=...` is passed again. `locals` are recomputed
# fresh on every plan, so there is nothing for Coder to pin.
vscode_extensions = [
{ id = "xdebug.php-debug", name = "PHP Debug", default = true },
{ id = "bmewburn.vscode-intelephense-client", name = "Intelephense", default = true },
{ id = "dbaeumer.vscode-eslint", name = "ESLint", default = true },
{ id = "esbenp.prettier-vscode", name = "Prettier", default = true },
{ id = "sanderronde.phpstan-vscode", name = "PHPStan", default = true },
{ id = "streetsidesoftware.code-spell-checker", name = "Code Spell Checker", default = true },
{ id = "stylelint.vscode-stylelint", name = "Stylelint", default = true },
{ id = "valeryanm.vscode-phpsab", name = "PHPSAB", default = true },
{ id = "biati.ddev-manager", name = "DDEV Manager", default = true },
{ id = "golang.go", name = "Go", default = true },
{ id = "deque-systems.vscode-axe-linter", name = "Axe Linter", default = false },
{ id = "andrewdavidblum.drupal-smart-snippets", name = "Drupal Smart Snippets", default = false },
{ id = "redhat.vscode-yaml", name = "YAML", default = false },
{ id = "sleistner.vscode-fileutils", name = "File Utils", default = false },
{ id = "GitHub.vscode-pull-request-github", name = "GitHub Pull Requests", default = false },
]
}
Loading