feat: template builder backend fixes (DEVEX-287) (#26432)

Part of the Template Builder wizard PR stack.

## Backend fixes

1. **Registry URL scheme fix**: Default
`CODER_TEMPLATE_BUILDER_REGISTRY_URL` was `https://registry.coder.com`
but Terraform module registry addresses must be scheme-less. Changed to
`registry.coder.com`.

2. **Sensitive variable defaults**: Module `.tf.tmpl` files for
claude-code, aider, amazon-q had sensitive `variable` blocks without
`default`, causing `terraform plan` to fail during template import. Also
fixed the `templatebuildermodulegen` script.

3. **Auto-quote string variables**: The backend now accepts raw string
values from callers and wraps them in HCL quotes automatically.
Previously callers were required to send pre-quoted HCL literals, which
is not a reasonable API contract.

---
> [!NOTE]
> Generated by Coder Agents on behalf of @jeremyruppel
This commit is contained in:
Jeremy Ruppel
2026-06-23 09:17:14 -04:00
committed by GitHub
parent eeb2624549
commit a30631198d
12 changed files with 73 additions and 68 deletions
+14 -25
View File
@@ -97,7 +97,7 @@ func TestMergeModuleVariables(t *testing.T) {
t.Run("CallerProvidesRequired", func(t *testing.T) {
t.Parallel()
merged, err := mergeModuleVariables(manifest, map[string]string{
"required_no_default": `"value"`,
"required_no_default": "value",
})
require.NoError(t, err)
require.Equal(t, `"value"`, merged["required_no_default"])
@@ -153,11 +153,11 @@ func TestMergeModuleVariables(t *testing.T) {
t.Run("InvalidStringValueRejected", func(t *testing.T) {
t.Parallel()
_, err := mergeModuleVariables(manifest, map[string]string{
"optional_no_default": "unquoted",
"optional_no_default": "${var.foo}",
})
require.Error(t, err)
require.Contains(t, err.Error(), `variable "optional_no_default"`)
require.Contains(t, err.Error(), "quoted string")
require.Contains(t, err.Error(), "interpolation")
})
t.Run("NullAcceptedForAnyType", func(t *testing.T) {
@@ -189,28 +189,17 @@ func TestValidateStringValue(t *testing.T) {
value string
wantErr string
}{
{"ValidEmpty", `""`, ""},
{"ValidSimple", `"hello"`, ""},
{"ValidPath", `"/home/coder"`, ""},
{"ValidURL", `"https://github.com/coder/coder"`, ""},
{"ValidLiteralBackslashN", `"line\\nbreak"`, ""},
{"ValidEscapedQuote", `"say \"hi\""`, ""},
{"ValidEscapedBackslash", `"path\\to\\file"`, ""},
{"ValidEmpty", "", ""},
{"ValidSimple", "hello", ""},
{"ValidPath", "/home/coder", ""},
{"ValidURL", "https://github.com/coder/coder", ""},
{"ValidWithQuotes", `say "hi"`, ""},
{"ValidWithNewlines", "line\nbreak", ""},
{"ValidWithBackslash", `path\to\file`, ""},
{"RejectedUnquoted", "hello", "quoted string"},
{"RejectedMissingOpenQuote", `hello"`, "quoted string"},
{"RejectedMissingCloseQuote", `"hello`, "quoted string"},
{"RejectedEmpty", "", "quoted string"},
{"RejectedSingleChar", `"`, "quoted string"},
{"RejectedUnescapedNewline", "\"line\nbreak\"", "unescaped newlines"},
{"RejectedCarriageReturn", "\"line\rbreak\"", "unescaped newlines"},
{"RejectedUnescapedQuote", `"say "hi""`, "unescaped quotes"},
{"RejectedHCLInterpolation", `"${var.foo}"`, "interpolation"},
{"RejectedHCLDirective", `"%{if true}yes%{endif}"`, "interpolation"},
{"RejectedOverlong", `"` + strings.Repeat("a", maxStringValueLen) + `"`, "maximum length"},
{"RejectedTrailingBackslash", `"test\"`, "trailing backslash"},
{"RejectedTrailingBackslashOnly", `"\"`, "trailing backslash"},
{"ValidEvenTrailingBackslashes", `"test\\\\"`, ""},
{"RejectedHCLInterpolation", "${var.foo}", "interpolation"},
{"RejectedHCLDirective", "%{if true}yes%{endif}", "interpolation"},
{"RejectedOverlong", strings.Repeat("a", maxStringValueLen+1), "maximum length"},
}
for _, tc := range tests {
@@ -313,7 +302,7 @@ func TestValidateVariableValue(t *testing.T) {
t.Run("UnsupportedTypeRejected", func(t *testing.T) {
t.Parallel()
v := ModuleVariable{Name: "test", Type: "list"}
err := validateVariableValue(v, `"val"`)
err := validateVariableValue(v, "val")
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported variable type")
})