You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Research: Automated Testing Approaches for coder-ddev and code.ddev.com
I've researched testing patterns across the Coder ecosystem and adjacent projects (coder/registry, DDEV add-ons, terraform test framework). Here's a comprehensive breakdown.
Current State
The repo has some testing infrastructure already but no CI pipelines:
make test — runs ddev --version, docker --version, node --version inside the built image. Validates binary presence only, not functional behavior.
drupal-core/scripts/test-issue-branches.sh — manual script that tests Drupal issue branch + composer resolution across D10/D11/D12. Runs locally, not in CI.
drupal-core/scripts/create-test-workspaces.sh — creates a matrix of test Coder workspaces (5 configurations covering D10/D11/D12 + issue forks). Has --create, --check, --delete modes. Also manual, not in CI.
No .github/workflows/ — no automated CI runs on push/PR.
bats-core, bats-support, bats-assert are already installed in the Docker image (via Homebrew) — unused.
Testing Layers
A comprehensive approach needs four layers. Each has different tooling and infrastructure requirements.
Layer 1: Static Validation (no live systems required)
terraform validate and terraform fmt --check should run on every PR. These catch HCL syntax errors and formatting issues.
Also useful: tflint for opinionated Terraform linting (deprecated resource arguments, required provider versions, etc.).
Layer 2: Terraform Unit Tests (no live Coder needed)
Terraform's native test framework (added in v1.6, mocking in v1.7) supports .tftest.hcl files that can run against mock providers. This is extremely useful for validating variable validation rules, required resource structure, and output correctness without requiring a live Coder deployment.
Example user-defined-web/tests/validate.tftest.hcl:
This approach is used by the coder/registry modules and runs in ~seconds on any Linux/macOS machine.
Layer 3: Shell Script Unit Tests (Docker required, no Sysbox)
The startup script logic in template.tf (the inline startup_script) can be extracted into a testable shell script and tested with bats-core, which is already installed in the image. The DDEV project uses this pattern extensively for their add-on tests (ddev/github-action-add-on-test).
What can be tested with bats (mocking external binaries that aren't available in a normal CI runner):
DDEV global_config.yaml is correctly placed at ~/.ddev/global_config.yaml
The startup script sets expected environment variables (DDEV_NONINTERACTIVE, DDEV_NO_INSTRUMENTATION, etc.)
File copy logic from /home/coder-files/ to /home/coder/ works
The key insight from Coder's docs on template testing: push with --activate=false, test against that specific version, then promote only on success. This prevents broken templates from affecting existing workspaces.
Authentication setup: Create a dedicated CI user and machine token:
coder users create --email ci@code.ddev.com --username ci-bot --login-type none
coder tokens create --user ci-bot --lifetime 8760h
# Store the token as CODER_SESSION_TOKEN in GitHub Actions secrets
What Tests to Write, Per Template
user-defined-web
Test
Layer
Verifies
terraform validate
Static
HCL syntax is valid
Variable validation (mock provider)
Terraform unit
Required variables enforced
make test (binary versions)
Image
ddev, docker, node present
startup script copies home files
Shell (bats)
/home/coder-files/ → /home/coder/ works
ddev --version after startup
Integration
DDEV is functional in workspace
docker ps after startup
Integration
DinD daemon is running
~/.ddev/global_config.yaml exists
Integration
DDEV config is initialized
ddev config && ddev start (PHP)
Integration
DDEV can start a project
drupal-core
This template has the most complex logic (issue branch detection, seed DB import, Composer resolution). The existing test-issue-branches.sh and create-test-workspaces.sh scripts should be the basis for CI tests here.
Test
Layer
Verifies
terraform validate
Static
HCL syntax valid
Issue branch parameter validation
Terraform unit
Branch format validated
Drupal project cloned and configured
Integration
~/drupal/web/index.php exists
Seed DB imported successfully
Integration
ddev drush status shows DB connected
Issue fork branches resolvable
Integration
Composer resolves issue branch
Works across D10, D11, D12
Integration
Matrix across drupal_version values
DDEV site accessible via port-forward
Integration
HTTP 200 from ddev describe URL
For the Drupal issue branch tests specifically, a matrix strategy is ideal:
Lighter template. Primarily needs the Traefik/routing behavior verified.
Test
Layer
Verifies
terraform validate
Static
HCL valid
Container starts and agent connects
Integration
Workspace reaches running state
Traefik routing works
Integration
Coder port-forward returns HTTP response
The Sysbox Constraint
The hardest part of full integration testing is that Sysbox cannot run on standard GitHub Actions runners. Sysbox requires kernel-level changes (installation on the host via .deb package) and cannot be installed at CI runtime without a privileged runner, which GitHub doesn't offer.
Options:
Self-hosted GitHub Actions runner on code.ddev.com's Sysbox-capable node — registers as runs-on: [self-hosted, sysbox]. This is the cleanest path. The runner creates real workspaces on the production or staging Coder deployment.
Scheduled tests on a dedicated staging Coder instance — run integration tests on a schedule (nightly) rather than on every PR. Use workflow_dispatch + a cron trigger. Keeps production clean.
Separate "canary" workspace — keep a persistent test workspace up, run verification checks against it on a schedule via coder ssh.
GitHub Actions with alternative DinD — for tests that don't need the full Sysbox stack (e.g., testing that DDEV works with a regular Docker daemon), use ddev/github-action-setup-ddev in a standard ubuntu-latest runner.
Suggested Phased Implementation
Phase 1 (no infrastructure needed): Add .github/workflows/validate.yml with terraform validate and terraform fmt --check for all three templates. Add terraform test with mock providers for variable validation.
Phase 2 (Docker only): Extract startup script into testable functions. Write bats tests for file initialization, DDEV config placement, PATH setup. Run in standard ubuntu-latest with Docker available.
Phase 3 (self-hosted runner): Set up a self-hosted GitHub Actions runner on the Sysbox-capable host. Add integration test workflow that creates/verifies/deletes test workspaces. Start with user-defined-web, add drupal-core matrix tests second.
Phase 4 (drupal-core specific): Move test-issue-branches.sh logic into the integration test workflow. Run the existing workspace creation matrix automatically on drupal-core template changes.
Infrastructure Prerequisites (one-time, on staging-coder.ddev.com)
Before any GitHub Actions CI runs against staging:
Register a self-hosted GitHub Actions runner on the staging host, tagged sysbox. Runs on the Sysbox-capable node, invokes coder CLI against the local Coder deployment.
Phase 1 — Static HCL Validation (no infrastructure required)
New files:.github/workflows/validate.yml Makefile additions:make validate, make fmt-check
Runs terraform validate + terraform fmt -check -recursive for all three templates on every PR and push to main. Uses ubuntu-latest — no credentials, no Coder, no Docker needed.
Risk: Zero.
Phase 2 — Terraform Mock Unit Tests (no infrastructure required)
New files:user-defined-web/tests/validate.tftest.hcl, drupal-core/tests/validate.tftest.hcl, freeform/tests/validate.tftest.hcl
Uses Terraform 1.7+ mock providers (available via hashicorp/setup-terraform@v3). Validates that required variables reject empty/missing values and that mandatory resources (docker_container, coder_agent) appear in the plan. Added to the same workflow as Phase 1.
Trigger: Every push/PR. Risk: Zero.
Phase 3 — Template Push Validation to Staging (--activate=false)
New files:.github/workflows/staging-validate.yml
Pushes all three templates to staging-coder.ddev.com with --activate=false. Catches Coder-provider-specific errors that terraform validate cannot (provider version compatibility, resource field validation against the live API). Cleans up the inactive test version after each run.
Trigger: Push to main, or workflow_dispatch. Runs on ubuntu-latest — only needs coder CLI + the staging credentials. No Sysbox. Risk: Low — no workspaces created, no impact on existing workspaces.
Phase 4 — Integration Tests: user-defined-web and freeform
New files:.github/workflows/integration-test.yml
Runs on the self-hosted sysbox runner. Creates a workspace from the current template version, verifies it, then deletes it. Cleanup runs in if: always() to prevent orphaned workspaces.
Trigger: Push to main, workflow_dispatch, or scheduled nightly (cron: '0 3 * * *'). Risk: Medium — creates/deletes real workspaces on staging. Always-cleanup step is critical.
Phase 5 — Integration Tests: drupal-core
Dependency:#99 (remove seed cache) should land first — it eliminates the cache_path required variable and all the cache-hit/miss branching, making this template straightforward to test.
New files:.github/workflows/drupal-integration-test.yml
Test matrix:
Test
drupal_version
issue_fork
Plain D10
10
(empty)
Plain D11
11
(empty)
Plain D12
12
(empty)
Issue fork D10
10
3380334
Issue fork D11
11
3515218
fail-fast: false so failures in one cell don't cancel others.
Plain installs take 5–10 minutes (no cache, full composer create-project). Total matrix runtime ~30–45 min.
Trigger: Scheduled nightly only (too slow for per-PR), plus workflow_dispatch. Changes to drupal-core/** also trigger. Risk: Medium — longer running, multiple concurrent workspaces. Nightly schedule with always-cleanup is the right tradeoff.
Order of Work
Set up staging self-hosted runner + CI bot user + GitHub secrets
drupal_version parameter validation — enforced by the Coder API (coder_parameter options), not Terraform; cannot be tested at the terraform test layer. No action needed.
Runner count — TODO comment left in integration-test.yml; revisit if more matrix cells or templates are added
Open question: do we need self-hosted runners at all?
It's possible none of the integration tests actually require a self-hosted runner. We do need a target Coder server (staging-coder.ddev.com) with Sysbox-capable provisioners — but the GitHub Actions jobs themselves only run coder CLI commands (coder templates push, coder create, coder ssh, curl). All of that could run on a standard ubuntu-latest GitHub-hosted runner as long as it has credentials to reach staging-coder.ddev.com.
The self-hosted runner was assumed necessary because of the Sysbox requirement, but Sysbox runs on the Coder provisioner host (the server where workspaces are created), not on the runner. The runner just talks to the Coder API.
If that's right, we could drop the runs-on: [self-hosted, sysbox] requirement entirely and use ubuntu-latest, which would simplify setup significantly (no runner registration, no runner maintenance).
Worth verifying before investing more time in runner infrastructure.
Activity
rfay commentedon Mar 23, 2026
Claude suggestions:
Research: Automated Testing Approaches for coder-ddev and code.ddev.com
I've researched testing patterns across the Coder ecosystem and adjacent projects (coder/registry, DDEV add-ons, terraform test framework). Here's a comprehensive breakdown.
Current State
The repo has some testing infrastructure already but no CI pipelines:
make test— runsddev --version,docker --version,node --versioninside the built image. Validates binary presence only, not functional behavior.drupal-core/scripts/test-issue-branches.sh— manual script that tests Drupal issue branch + composer resolution across D10/D11/D12. Runs locally, not in CI.drupal-core/scripts/create-test-workspaces.sh— creates a matrix of test Coder workspaces (5 configurations covering D10/D11/D12 + issue forks). Has--create,--check,--deletemodes. Also manual, not in CI..github/workflows/— no automated CI runs on push/PR.Testing Layers
A comprehensive approach needs four layers. Each has different tooling and infrastructure requirements.
Layer 1: Static Validation (no live systems required)
terraform validateandterraform fmt --checkshould run on every PR. These catch HCL syntax errors and formatting issues.Also useful:
tflintfor opinionated Terraform linting (deprecated resource arguments, required provider versions, etc.).Layer 2: Terraform Unit Tests (no live Coder needed)
Terraform's native test framework (added in v1.6, mocking in v1.7) supports
.tftest.hclfiles that can run against mock providers. This is extremely useful for validating variable validation rules, required resource structure, and output correctness without requiring a live Coder deployment.Example
user-defined-web/tests/validate.tftest.hcl:This approach is used by the
coder/registrymodules and runs in ~seconds on any Linux/macOS machine.Layer 3: Shell Script Unit Tests (Docker required, no Sysbox)
The startup script logic in
template.tf(the inlinestartup_script) can be extracted into a testable shell script and tested with bats-core, which is already installed in the image. The DDEV project uses this pattern extensively for their add-on tests (ddev/github-action-add-on-test).What can be tested with bats (mocking external binaries that aren't available in a normal CI runner):
global_config.yamlis correctly placed at~/.ddev/global_config.yamlDDEV_NONINTERACTIVE,DDEV_NO_INSTRUMENTATION, etc.)/home/coder-files/to/home/coder/worksWELCOME.txtis present after initialization$PATHExample
tests/startup.bats:This requires refactoring the inline startup script into sourced functions — which would also improve maintainability of the template itself.
Layer 4: Integration Tests (live Coder + Sysbox host required)
This is the most valuable but also most constrained layer. It requires:
The canonical Coder CI pattern uses the official
coder/setup-actionGitHub Action:The key insight from Coder's docs on template testing: push with
--activate=false, test against that specific version, then promote only on success. This prevents broken templates from affecting existing workspaces.Authentication setup: Create a dedicated CI user and machine token:
coder users create --email ci@code.ddev.com --username ci-bot --login-type none coder tokens create --user ci-bot --lifetime 8760h # Store the token as CODER_SESSION_TOKEN in GitHub Actions secretsWhat Tests to Write, Per Template
user-defined-webterraform validatemake test(binary versions)/home/coder-files/→/home/coder/worksddev --versionafter startupdocker psafter startup~/.ddev/global_config.yamlexistsddev config && ddev start(PHP)drupal-coreThis template has the most complex logic (issue branch detection, seed DB import, Composer resolution). The existing
test-issue-branches.shandcreate-test-workspaces.shscripts should be the basis for CI tests here.terraform validate~/drupal/web/index.phpexistsddev drush statusshows DB connecteddrupal_versionvaluesddev describeURLFor the Drupal issue branch tests specifically, a matrix strategy is ideal:
freeformLighter template. Primarily needs the Traefik/routing behavior verified.
terraform validaterunningstateThe Sysbox Constraint
The hardest part of full integration testing is that Sysbox cannot run on standard GitHub Actions runners. Sysbox requires kernel-level changes (installation on the host via
.debpackage) and cannot be installed at CI runtime without a privileged runner, which GitHub doesn't offer.Options:
runs-on: [self-hosted, sysbox]. This is the cleanest path. The runner creates real workspaces on the production or staging Coder deployment.workflow_dispatch+ a cron trigger. Keeps production clean.coder ssh.ddev/github-action-setup-ddevin a standardubuntu-latestrunner.Suggested Phased Implementation
Phase 1 (no infrastructure needed): Add
.github/workflows/validate.ymlwithterraform validateandterraform fmt --checkfor all three templates. Addterraform testwith mock providers for variable validation.Phase 2 (Docker only): Extract startup script into testable functions. Write bats tests for file initialization, DDEV config placement, PATH setup. Run in standard
ubuntu-latestwith Docker available.Phase 3 (self-hosted runner): Set up a self-hosted GitHub Actions runner on the Sysbox-capable host. Add integration test workflow that creates/verifies/deletes test workspaces. Start with
user-defined-web, adddrupal-corematrix tests second.Phase 4 (drupal-core specific): Move
test-issue-branches.shlogic into the integration test workflow. Run the existing workspace creation matrix automatically ondrupal-coretemplate changes.References
rfay commentedon Apr 30, 2026
Finalized Implementation Plan
This comment captures the agreed plan from design discussion. Decisions made:
drupal-coreseed cache is being removed in refactor: simplify drupal-core seed cache (unnecessary complexity) #99; plain-vanilla Drupal tests (::10,::11,::12) will be added to CI once that lands. Until then, Phase 5 focuses on issue-fork tests.Infrastructure Prerequisites (one-time, on staging-coder.ddev.com)
Before any GitHub Actions CI runs against staging:
sysbox. Runs on the Sysbox-capable node, invokescoderCLI against the local Coder deployment.STAGING_CODER_URL=https://staging-coder.ddev.comSTAGING_CODER_SESSION_TOKEN= token from step 2Phase 1 — Static HCL Validation (no infrastructure required)
New files:
.github/workflows/validate.ymlMakefile additions:
make validate,make fmt-checkRuns
terraform validate+terraform fmt -check -recursivefor all three templates on every PR and push tomain. Usesubuntu-latest— no credentials, no Coder, no Docker needed.Risk: Zero.
Phase 2 — Terraform Mock Unit Tests (no infrastructure required)
New files:
user-defined-web/tests/validate.tftest.hcl,drupal-core/tests/validate.tftest.hcl,freeform/tests/validate.tftest.hclUses Terraform 1.7+ mock providers (available via
hashicorp/setup-terraform@v3). Validates that required variables reject empty/missing values and that mandatory resources (docker_container,coder_agent) appear in the plan. Added to the same workflow as Phase 1.Trigger: Every push/PR.
Risk: Zero.
Phase 3 — Template Push Validation to Staging (
--activate=false)New files:
.github/workflows/staging-validate.ymlPushes all three templates to staging-coder.ddev.com with
--activate=false. Catches Coder-provider-specific errors thatterraform validatecannot (provider version compatibility, resource field validation against the live API). Cleans up the inactive test version after each run.Trigger: Push to
main, orworkflow_dispatch. Runs onubuntu-latest— only needscoderCLI + the staging credentials. No Sysbox.Risk: Low — no workspaces created, no impact on existing workspaces.
Phase 4 — Integration Tests:
user-defined-webandfreeformNew files:
.github/workflows/integration-test.ymlRuns on the self-hosted
sysboxrunner. Creates a workspace from the current template version, verifies it, then deletes it. Cleanup runs inif: always()to prevent orphaned workspaces.Verifications for
user-defined-web:Verifications for
freeform: Workspace reachesrunningstate, agent connects, port-forward returns HTTP 200.Trigger: Push to
main,workflow_dispatch, or scheduled nightly (cron: '0 3 * * *').Risk: Medium — creates/deletes real workspaces on staging. Always-cleanup step is critical.
Phase 5 — Integration Tests:
drupal-coreDependency: #99 (remove seed cache) should land first — it eliminates the
cache_pathrequired variable and all the cache-hit/miss branching, making this template straightforward to test.New files:
.github/workflows/drupal-integration-test.ymlTest matrix:
drupal_versionissue_fork33803343515218fail-fast: falseso failures in one cell don't cancel others.Verifications:
Plain installs take 5–10 minutes (no cache, full
composer create-project). Total matrix runtime ~30–45 min.Trigger: Scheduled nightly only (too slow for per-PR), plus
workflow_dispatch. Changes todrupal-core/**also trigger.Risk: Medium — longer running, multiple concurrent workspaces. Nightly schedule with always-cleanup is the right tradeoff.
Order of Work
user-defined-web+freeformdrupal-corenightly matrix testsci: add HCL validate/fmt workflow and Makefile targets for #71 (Phase…
ci: add Terraform mock unit tests for all templates for #71 (Phase 2) (…
ci: add staging push validation workflow for #71 (Phase 3)
ci: add staging push validation workflow for #71 (Phase 3) (#103)
ci: add make archive-inactive-versions and pull_request trigger for #71
ci: add drupal-core matrix, unzip doc, and vscode_extensions param for …
ci: cancel stale runs and document parallel runner setup for #71 (Pha…
ci: fix --wait flag syntax in coder ssh for #71 (Phase 4)
ci: fix tilde expansion in remote coder ssh command for #71 (Phase 4)
ci: fix drupal-core params, freeform global_config race, and unzip do…
ci: fix startup race for global_config and drupal-core params for #71 …
fix: create ddev global_config.yaml early via ddev config global for #71
ci: use system coder binary instead of coder/setup-action for #71 (Ph…
18 remaining items
ci: use bash heredoc with ddev start stdin fix for #71 (Phase 4)
ci: fix coder ssh heredoc+PTY hang — use scp to push script file for #71
ci: add integration test workflow for user-defined-web, freeform, dru…
ci: run drupal integration tests on pull requests, for #71
ci: fix template version name collision in drupal-plain matrix, for #71
ci: fix web root path, add branch/URL verification, for #71
rfay commentedon May 1, 2026
Status update — remaining work
Done (this branch, PR #113)
user-defined-webandfreeformdrupal-coreintegration tests — plain D11/D12 matrix plus issue fork jobDRUPAL_TEST_ISSUE_FORKrepo variable neededdrupal-site--...coder_app proxy) anduser-defined-web(viaddev-web--...proxy)Remaining
freeformintegration test improvements — deferred to "freeform" template struggles with port 80, switch to 8080 #87drupal_versionparameter validation — enforced by the Coder API (coder_parameteroptions), not Terraform; cannot be tested at theterraform testlayer. No action needed.integration-test.yml; revisit if more matrix cells or templates are addedOpen question: do we need self-hosted runners at all?
It's possible none of the integration tests actually require a self-hosted runner. We do need a target Coder server (staging-coder.ddev.com) with Sysbox-capable provisioners — but the GitHub Actions jobs themselves only run
coderCLI commands (coder templates push,coder create,coder ssh,curl). All of that could run on a standardubuntu-latestGitHub-hosted runner as long as it has credentials to reach staging-coder.ddev.com.The self-hosted runner was assumed necessary because of the Sysbox requirement, but Sysbox runs on the Coder provisioner host (the server where workspaces are created), not on the runner. The runner just talks to the Coder API.
If that's right, we could drop the
runs-on: [self-hosted, sysbox]requirement entirely and useubuntu-latest, which would simplify setup significantly (no runner registration, no runner maintenance).Worth verifying before investing more time in runner infrastructure.
ci: fix tilde expansion in coder ssh commands, for #71
ci: fix DDEV test step gating and cleanup command, for #71
ci: fix branch carriage return and curl redirect for Drupal URL check…
fix: pass Coder session token for drupal-site URL check, add diagnost…
ci: add drupal integration test workflow with issue fork support, for #…
rfay commentedon May 6, 2026
This is mostly where it should be. Keeping it open for suggestions about future basics.
rfay commentedon May 10, 2026
In great shape and very useful, closing