Skip to content

feat(coderd): add Coder Quickstart base to the template builder - #27247

Draft
nickvigilante wants to merge 10 commits into
mainfrom
vigilante/docs-558-add-coder-quickstart-template-to-template-builder
Draft

feat(coderd): add Coder Quickstart base to the template builder#27247
nickvigilante wants to merge 10 commits into
mainfrom
vigilante/docs-558-add-coder-quickstart-template-to-template-builder

Conversation

@nickvigilante

@nickvigilante nickvigilante commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the Coder Quickstart template as a selectable base in the template builder guided wizard. It is a Docker-based starter that lets a user pick languages and optionally clone a repo, and it is grouped next to the Docker base in the base list.

What changed

  • New base package coderd/templatebuilder/bases/quickstart/:
    • base.json: id: "quickstart", display_name: "Coder Quickstart", os: "linux", and included_modules: ["git-clone"] (see the collision guard below).
    • main.tf.tmpl: a Docker workspace with a language selector, a single language-install script, an optional Git clone, and workspace presets. Editors are added via the builder's module step rather than baked into the base.
    • install-languages.sh.tftpl and README.md (with prerequisites markers so the builder can extract the prerequisites section).
  • Placement: bases are ordered alphabetically by display name, then the Coder Quickstart base is grouped immediately before the Docker base. There is no first-position pin.
  • Base/module collision guard: a base declares the catalog module IDs it already bundles via included_modules in base.json. validateModules seeds its seen-set from those IDs, so a wizard-selected module that the base already renders is rejected with a clear error, and the modules endpoint omits it so the wizard never offers a colliding module. A test asserts each base's included_modules matches the catalog modules it actually renders, so the manifest cannot silently drift from the Terraform.

Testing

  • go test ./coderd/: pass (bases ordering, the baseSpec table, and the modules-endpoint base filter).
  • go test ./coderd/templatebuilder/ including -race -shuffle=on: pass (all-bases render/snapshot, the collision guard, and the included_modules invariant).
  • gofmt, go vet, terraform fmt, typos, and make lint/emdash: clean.

Review decisions

  • Template scope: trimmed to infrastructure, a language selector, and an optional Git clone. The IDE selector was removed; editors are added via the builder's module step.
  • Placement: grouped next to the Docker base rather than pinned first.
  • git-clone in the base: kept for the build-time clone affordance. The base renders its module source verbatim, so it currently pins the public registry and does not yet honor a deployment's registry mirror; that fix is tracked in DOCS-610 and delivered as a stacked follow-up PR (an in-code note documents the current behavior).
  • Container image: left hardcoded (codercom/enterprise-base:ubuntu). The quickstart base is the opinionated path; the Docker base covers custom images. Revisiting a container_image variable is tracked in DOCS-611.

Linear: DOCS-558

This PR was created with AI assistance (Coder Agents).

@linear-code

linear-code Bot commented Jul 14, 2026

Copy link
Copy Markdown

DOCS-558

@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-08-04 19:33 UTC by @nickvigilante
Spend: $56.63 / $100.00

Review history
  • R1 (2026-07-14), 1 P0, COMMENT. Review
  • R2 (2026-07-14): 12 reviewers, 5 Note, 1 P0, 1 P1, 2 P2, 1 P3, REQUEST_CHANGES. Review
  • R3 (2026-07-15), 5 Note, 1 P0, 1 P1, 2 P2, 1 P3, COMMENT. Review
  • R4 (2026-07-21): 16 reviewers, 1 Nit, 6 Note, 1 P0, 1 P1, 3 P2, 5 P3, REQUEST_CHANGES. Review
  • R5 (2026-08-04): 12 reviewers, 2 Nit, 8 Note, 1 P0, 1 P1, 3 P2, 7 P3, COMMENT. Review
  • R6 (2026-08-04): 12 reviewers, 2 Nit, 9 Note, 1 P0, 1 P1, 3 P2, 8 P3, COMMENT. Review

deep-review v0.9.0 | Round 6 | 1142706..bc740d2

Last posted: Round 6, 24 findings (1 P0, 1 P1, 3 P2, 8 P3, 2 Nit, 9 Note), COMMENT. Review

Finding inventory

Finding inventory - PR #27247

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P0 Author fixed (58eb2c9) coderd/templatebuilder_handler.go:91 Quickstart-first pin breaks pre-existing TestTemplateBuilderBases/Sorted R1 Netero Yes
CRF-2 Note Author fixed (58eb2c9) coderd/templatebuilder_handler_test.go:110 No positive test asserts the new quickstart-first ordering; description's test claim ran the wrong package R1 Netero Yes (body)
CRF-3 Note Author acknowledged R3; base rewritten R4 (re-evaluate) coderd/templatebuilder/testdata/quickstart.tf.golden:1 Golden is byte-identical to embedded main.tf.tmpl (directive-free); snapshot asserts source==source. Low value, not vacuous R2 Netero, Law Yes
CRF-4 P1 Author fixed (30fb31b); verified R4 (instance); class durability tracked as CRF-12 coderd/templatebuilder/bases/quickstart/main.tf.tmpl:273 Base embeds catalog-named module blocks; selecting the same module in the wizard yields a duplicate module call and a broken template. No collision guard in validateModules R2 Hisoka P1, Mafuuu P1, Pariston P1, Luffy P1, Meruem P1, Bisky P2 Yes
CRF-5 P2 Author fixed (17186ff); verified R4 coderd/templatebuilder/bases/quickstart/main.tf.tmpl:118 JetBrains IDE selector + preset + README promise install, but the jetbrains module is commented out; silent no-op R2 Leorio P2, Hisoka Note Yes
CRF-6 P2 Deferred (DOCS-610; stacked follow-up PR; in-code note added 658b74b) coderd/templatebuilder/bases/quickstart/main.tf.tmpl:157 Base git-clone module hardcodes registry.coder.com; BaseRenderContext structurally cannot honor RegistryURL; mirror/air-gapped git_repo builds fail terraform init R2 Mafuuu P2, Chopper P2, Melody P2, Meruem P2, Luffy P2, Knov P2, Pariston P3, Hisoka Note, Zoro Note Yes
CRF-7 Note Author accepted R5 (DOCS-611; opinionated starter, baseSpec locks no vars) coderd/templatebuilder/bases/quickstart/base.json:5 Container image hardcoded while sibling bases expose a container_image variable (open q#3) R2 Mafuuu, Luffy Yes
CRF-8 Note Superseded by R4 ordering change; panel closed R4 (mechanism now groupQuickstartBeforeDocker, proportional) coderd/templatebuilder_handler.go:87 Quickstart-first ordering is a hardcoded ID pin in the comparator, not data (open q#2). Proportional for a single pin R2 Pariston, Meruem Yes
CRF-9 Note Author fixed (76aabd3); verified R4; modernization half tracked as CRF-18 coderd/templatebuilder_handler.go:91 Comparator uses unstable reflection-based sort.Slice; not total (no ID tiebreak). Consider slices.SortFunc + tiebreak on ID R2 Komugi Note, ging-go Nit Yes
CRF-10 P3 Author fixed main.tf.tmpl (17186ff); author contested R5; panel closed R5 (8/8 accept: base diverged, drift covered by CRF-12) coderd/templatebuilder/bases/quickstart/install-languages.sh.tftpl:1 Base is a byte-for-byte copy of the already-embedded example; nothing guards drift R2 Robin Yes
CRF-11 Note Author fixed (b64ceea); verified R4 coderd/templatebuilder_handler_test.go:44 Quickstart absent from handler specs table; API surface only checked by generic loops R2 Bisky Yes
CRF-12 P3 Author fixed (657abbb); panel verified R5; default-context limitation -> CRF-21 coderd/templatebuilder/bases/quickstart/base.json:6 included_modules is a hand-maintained duplicate of the base's rendered module blocks; nothing enforces the mirror, so drift silently re-opens CRF-4 (P1). No catalog-ID validation either R4 Hisoka P3, Mafuuu P3, Pariston P3, Chopper P3, Melody P3, Meruem P3, Zoro P3, Knov P2 Yes
CRF-13 P2 Author fixed (658b74b, doc); panel verified R5; behavioral residual -> CRF-23 coderd/templatebuilder/bases/quickstart/README.md:49 README promises languages are cached for subsequent starts, but installs land on the ephemeral container and reinstall every start (blocking login); fails on mirror/air-gapped R4 Leorio P2, Meruem P2 Yes
CRF-14 P3 Author fixed (658b74b); panel verified R5 coderd/templatebuilder/bases/quickstart/install-languages.sh.tftpl:14 Language dispatch uses unanchored grep substring match; safe only because current values are substring-disjoint; a future value silently mis-installs R4 Meruem P3, Hisoka Note, Mafuuu Note, Knov Note Yes
CRF-15 P3 Author fixed (657abbb); panel verified R5 coderd/templatebuilder_handler_test.go:224 BaseExcludesIncludedModules quickstart loop is a vacuous negative loop; passes on an empty list. Add require.NotEmpty R4 Bisky Yes
CRF-16 P3 Author fixed (1cf64f1); panel verified R5; minor comment residual -> CRF-24 coderd/templatebuilder_handler.go:87 Base/module collision rationale duplicated ~6 times across the diff; comments narrate the code and will drift R4 Gon P2 Yes
CRF-17 Note Author fixed (1cf64f1); panel verified R5 coderd/templatebuilder/compose.go:236 Conflict loop iterates only requested modules, so a base-included module's own ConflictsWith is never enforced; the "bidirectional" comment overpromises. Latent R4 Mafuuu, Melody Yes
CRF-18 Nit Author fixed (1cf64f1); panel verified R5 coderd/templatebuilder_handler.go:91 Go modernization: slices.SortFunc + cmp.Or, slices.IndexFunc, slices.Insert over hand-rolled sort/search/splice R4 ging-go Nit, Robin Nit Yes
CRF-19 Note Open R6; test-only + fails loud (Hisoka empirically confirmed); structural fix available coderd/templatebuilder/render.go:174 ExtractParameterOptionValues/ExtractModuleNames hand-roll HCL parsing (brace-depth scan desyncs on braces-in-strings/heredocs; regexes don't skip comments), untested. Test-only, nil->require.NotEmpty fails loud. Fix: use hclsyntax (already imported) or a table test R5 Netero, Bisky, Mafu-san, Mafuuu, Zoro, Hisoka (Note); Robin (P3 structural) Yes
CRF-20 P3 Author fixed (bc740d2); panel verified closed R6 (mutation-tested) coderd/templatebuilder/bases/quickstart/main.tf.tmpl:44 Language selector option values and install-script has_language branches are hand-maintained duplicates across two files with no cross-check test; drift silently no-ops a selected language (CRF-5 class). Same shape CRF-12 just guarded for included_modules R5 Bisky P3 Yes
CRF-21 Note Author addressed (bc740d2); panel verified accurate R6 coderd/templatebuilder/compose_test.go:575 CRF-12 invariant test renders with DefaultBaseRenderContext only; production renderBase overlays merged variables, so a variable-gated catalog module block would evade the guard. Dormant today (quickstart has no vars) R5 Hisoka Yes
CRF-22 Note Author fixed (bc740d2); panel verified accurate R6 coderd/templatebuilder/bases/quickstart/README.md:49 CRF-13 fix's leading "reinstalled on every start" is not literally true for Rust ($HOME/.cargo persists); the trailing "most" hedges it. Leorio (docs) considers current wording acceptable R5 Mafuuu Note; Leorio/Hisoka disagree (acceptable) Yes
CRF-23 P3 Deferred (DOCS-614, Medium; distinct from DOCS-610) coderd/templatebuilder/bases/quickstart/main.tf.tmpl:141 CRF-13's doc fix is honest but the behavioral residual (reinstall-every-start blocks login; fails on network-restricted deployments) is untracked; DOCS-610 covers git-clone registry, not this. Needs human decision: track or accept R5 Mafu-san P3 Yes
CRF-24 Nit Author fixed (bc740d2); panel verified closed R6 coderd/templatebuilder_handler.go:90 CRF-16 residual: the sort-site comment still repeats the groupQuickstartBeforeDocker placement rationale verbatim R5 Gon Nit Yes
CRF-25 P3 Open coderd/templatebuilder/bases/quickstart/main.tf.tmpl:174 The four coder_workspace_preset blocks are a third hand-maintained list of language values; the CRF-20 guard binds only selector<->script, leaving presets unbound. A preset naming a drifted value silently no-ops (or fails at build). Extend the test to assert each preset's languages subset of selector options R6 Pariston P3, Melody P3 Yes

Law analysis

  • Effective LOC: +1049 -1 (8 files); ~13 lines of novel logic, rest is verbatim-ported template data plus a byte-identical golden duplicate.
  • Head SHA: 3a6ad9a
  • Verdict: Don't split.
  • Enforcement: Advisory (no action). Panel proceeds.
  • Panel note (for future panel round): testdata/quickstart.tf.golden is byte-for-byte identical to bases/quickstart/main.tf.tmpl (template has no directives, so render is identity). The snapshot test therefore asserts "the embedded file equals the embedded file rendered." Weak but not vacuous; catches accidental corruption of the embedded template. Worth a panel look on the next round.

Contested and acknowledged

CRF-10 (P3, install-languages.sh.tftpl) - base-vs-example verbatim-copy drift

  • Finding (R2, Robin): the base file was a byte-for-byte copy of examples/templates/quickstart/install-languages.sh.tftpl with no test guarding drift.
  • Author defense (R5): after 658b74b the base file was rewritten (dispatch changed to has_language) so it is no longer byte-identical to the example; the base is intentionally its own artifact; a base==example test would wrongly block legitimate divergence; the real manifest-vs-rendered drift risk is covered by the new CRF-12 invariant test.
  • Panel closure (R5, 8/8 accept): Netero, Hisoka, Mafu-san, Mafuuu, Pariston, Robin, Melody, Razor each diffed the two files, confirmed genuine divergence (base uses has_language, example still uses grep -q), and agreed a base==example guard would block legitimate divergence while the real drift is caught by CRF-12. Closed. (Sibling note, out of PR scope: the example file retains the old unanchored-grep CRF-14 bug; port has_language to the example in its own change.)

Round log

Round 6

Churn guard: PROCEED. Author pushed bc740d2 (PR rebased onto 1142706). Panel (12) + Netero verified CRF-20/21/22/24 closed (CRF-20 mutation-tested by 6 reviewers; CRF-20 = ExtractParameterOptionValues + TestQuickstartLanguageSelectorMatchesInstallScript, CRF-21 = documented default-context limitation, CRF-22 = README names Rust exception, CRF-24 = sort-site comment reduced). CRF-23 deferred (DOCS-614), CRF-6/7 tracked, out-of-scope example grep tracked (DOCS-613), slices.IsSorted declined with reasoning. New: CRF-25 (P3, presets are an unguarded third language list; Pariston + Melody convergent; verified 4 presets, no preset test). CRF-19 (Note): the test-only ExtractParameterOptionValues/ExtractModuleNames hand-roll HCL parsing with untested brace/comment handling; Hisoka empirically confirmed an unbalanced brace returns nil -> require.NotEmpty fails loud, so blast radius is a loud test failure, not a shipped bug (Robin rated it P3 as a structural alternative: use hclsyntax, already imported; downgraded to Note per the empirical fail-safe bounding). No new P0/P1/P2. Event COMMENT + dismiss stale CHANGES_REQUESTED. Reviewed against 1142706..bc740d2.

Round 5

Churn guard: PROCEED. Author pushed 657abbb, 1cf64f1, 658b74b (PR rebased onto 5f3b8755). All 7 R4 fixes (CRF-12/13/14/15/16/17/18) verified clean by the panel (12 reviewers) + Netero. CRF-10 contest closed in the author's favor (unanimous 8/8). CRF-6 (DOCS-610) and CRF-7 (DOCS-611) deferred/accepted with tickets; in-code note verified accurate; not re-raised. Panel (12): Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Gon, Leorio, ging-go, Komugi, Melody, Robin + wildcard Razor. New: CRF-20 (P3 language-selector drift guard, Bisky), CRF-23 (P3 CRF-13 behavioral residual untracked, Mafu-san), CRF-21/CRF-22/CRF-19 Notes, CRF-24 Nit. No new P0/P1/P2. Verified CRF-20 (6 param values vs 6 has_language branches, no binding test) in worktree. Event COMMENT + dismiss the stale R2/R4 CHANGES_REQUESTED (no blocking findings remain). Reviewed against 5f3b8755..658b74b.

Round 4

Churn guard: PROCEED. Scope owner (@david-fraley) decided open q#1: move next to Docker, remove the IDE selector. Author implemented across b64ceea, 17186ff, 30fb31b: trimmed the base to infra + language selector + optional git-clone (removed ides/jetbrains params, all five IDE module blocks, presets), added a real collision guard (included_modules in base.json seeding validateModules, plus modules-endpoint filtering), changed ordering to group quickstart before docker (no first-position pin), and added the quickstart baseSpec row (CRF-11). All 5 open findings addressed by code. Panel to verify: CRF-4 (guard adequacy), CRF-6 (surviving git-clone registry hardcode). Significant restructure -> Gon + Leorio re-triggered. Reviewed against 8eaf4f5..30fb31b.
Panel (16): Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Gon, Leorio, ging-go, Komugi, Chopper, Melody, Meruem, Robin, Luffy + wildcards Zoro, Knov. Netero advisory: no findings. Verified fixed: CRF-1/2/4/5/8/9/11. CRF-4 collision guard works on both paths (compose reject + endpoint filter, tested), but the class is only half-closed -> CRF-12. New findings: CRF-6 re-raised P2 (6 reviewers; git-clone hardcode, BaseRenderContext has no RegistryBase, verified in worktree), CRF-12 P3 (8 reviewers; included_modules not enforced vs rendered blocks), CRF-13 P2 (README caching false promise), CRF-14 P3 (unanchored grep), CRF-15 P3 (vacuous test loop), CRF-16 P3 (comment duplication), CRF-17 Note (ConflictsWith half-pairing), CRF-18 Nit (Go modernization), CRF-10 re-raised P3 (install-languages.sh drift). CRF-3/CRF-7 acknowledged, no action. Event REQUEST_CHANGES (P2 present).

Round 3

Churn guard: BLOCKED. 1 addressed (CRF-9, ID tiebreak in 76aabd3, verified in worktree), 3 acknowledged (CRF-3/7/8), 4 contested and held pending open-question-#1 scope decision escalated to the scope owner (CRF-4/5/6/10, no ticket), 1 silent (CRF-11: no response, no code change; it was folded into the round-2 body with no thread). Per the verdict rules one silent finding blocks. No panel, no Netero. Posted BLOCKED status COMMENT (prior round-2 CHANGES_REQUESTED still stands). Reviewed against 8eaf4f5..76aabd3.

Round 2

Churn guard: PROCEED. CRF-1 and CRF-2 both addressed by commit 58eb2c9 (Sorted rewritten to assert quickstart-first + remaining alphabetical; positive assertion added). Panel proceeds.
Panel (12): Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Gon, Leorio, ging-go, Komugi, Robin, Luffy + wildcard Meruem. Netero re-ran (advisory once panel gates cleared): 1 new Note (CRF-3). Law did not re-run (effective additions grew only 7).
New findings: CRF-4 (P1 module collision, 6 reviewers convergent), CRF-5 (P2 JetBrains dead selector), CRF-6 (P2 registry hardcoded), CRF-10 (P3 verbatim-copy drift), CRF-7/8/9/11 Notes, CRF-3 Note. CRF-4/5/6 verified in orchestrator worktree (module blocks, commented-out jetbrains source, hardcoded registry, quickstart unique among bases). Process notes in body: CRF-2 clause-2 re-raise (Mafu-san P3, description still cites wrong test package; author acknowledged in reply), emdashes in PR body (Nit), README not byte-verbatim (prettier). Event REQUEST_CHANGES (P1 present). Reviewed against 8eaf4f5..58eb2c9.

Round 1

Netero-only first-pass gate. Law ran (effective additions 1049 > 1000): verdict Don't split, advisory. Netero found 1 P0, 1 Note. P0 verified by running go test ./coderd/ -run TestTemplateBuilderBases/Sorted in the orchestrator worktree (FAIL confirmed). Panel skipped per Netero decision gate (P0 present, pre-panel round). Posted Netero-only COMMENT review. Reviewed against 8eaf4f5..3a6ad9a.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First-pass review (Netero) only. The full reviewer panel has not yet reviewed this PR; it will run once the mechanical findings below are addressed. This is a clean, well-scoped scaffold: the base package is embedded through the existing //go:embed bases directive, the port from examples/templates/quickstart is faithful, and the open questions are correctly held inside one draft PR rather than spread across several. Law assessed the 1049-LOC headline and recommends against splitting: about 13 lines are novel logic and the rest is verbatim-ported template data plus a byte-identical golden duplicate.

Severity count: 1 P0, 1 Note.

Blocking issue: the quickstart-first ordering pin deterministically breaks the pre-existing TestTemplateBuilderBases/Sorted test in package coderd. Verified by running the test in a clean worktree. The PR introduces a new ordering contract (quickstart first, rest alphabetical) but leaves the test asserting the old contract (strictly alphabetical across all bases).

Note (process): the PR description reports go test ./coderd/templatebuilder/ passing, but the failing Sorted assertion lives in package coderd (coderd/templatebuilder_handler_test.go), which that command never exercised. Running the package that contains the handler test would have surfaced this. There is also no positive test asserting the new quickstart-first ordering; add one alongside fixing Sorted.

For the panel's next round, Law flagged that testdata/quickstart.tf.golden is byte-for-byte identical to bases/quickstart/main.tf.tmpl because the template carries no Go-template directives, so as Law put it, the golden test asserts only "the file we embedded equals the file we embedded." Weak but not vacuous; the panel will take a closer look after the P0 is fixed.

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/templatebuilder_handler.go Outdated
@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full panel review (round 2). The prior-round fixes are verified closed: the Sorted test now asserts resp.Bases[0].ID == "quickstart" and checks the remainder is name-sorted from index 2, which addresses the root cause of CRF-1, and the CRF-2 positive assertion is present. The ordering pin itself is clean and minimal, the port is faithful, and the open questions are disclosed honestly in a draft marked do-not-merge. Law's verdict stands: don't split.

Severity count: 1 P1, 2 P2, 1 P3, and several Notes/Nits.

The central issue is that the panel converged, from five independent angles, on one root cause: this is a fully-composed, batteries-included template shipped in the base slot, and the builder's model is base (infra) + modules composed on top. As Luffy put it, "It's not a base. It's a whole finished template wearing a base costume." That mismatch produces the P1 collision (CRF-4), the P2 registry hardcoding (CRF-6), the P2 dead JetBrains selector (CRF-5), and the P3 verbatim-copy drift (CRF-10). Open question #1, option (a) (strip modules/presets so the base is pure infra) dissolves all four at once. Verified in a worktree: quickstart is the only base that embeds catalog-named module blocks; the siblings declare at most azure_region/gcp_region, which are not catalog IDs, so this hazard is introduced by this PR, not pre-existing.

Independently of the #1 decision, the compose pipeline has no guard for base-declared module names colliding with wizard-selected ones. validateModules only dedupes the requested list. Even if this base is trimmed, the next base that legitimately bundles a catalog module hits the same wall. Pariston and Meruem propose the durable fix: declare the base's included module IDs (e.g. included_modules in base.json) and seed validateModules' seen-map from it, so the disjoint-namespace invariant is enforced by the same mechanism that already rejects duplicate requests.

The draft status and the open-questions block do not close these findings. There is no ticket for #1, #2, or #3, and a paragraph in the description is not a resolution. Each needs a human decision: fix it here, file a tracking issue, or explicitly accept the gap.

Process notes (not blocking): the Testing section still lists only go test ./coderd/templatebuilder/, which cannot exercise the ordering logic or its new assertion (both live in package coderd); the author acknowledged this in a reply but the PR body is unchanged, so update it to cite go test ./coderd/. The PR description uses emdashes throughout; the shipped files are clean so make lint/emdash will not fail, but the body violates the house standard. Minor accuracy: the README is not byte-verbatim (prettier reformatted the table separators), so "verbatim + markers" slightly overstates it.


coderd/templatebuilder_handler_test.go:44

Note [CRF-11] Quickstart is absent from the handler-level specs table, so its API surface is only checked by generic loops (Bisky).

TestTemplateBuilderBases/OK asserts name, icon, OS, and variable presence for a curated specs list; quickstart is not in it, so the handler never asserts which variables it exposes (currently none). Given open question #3 (whether Quickstart should expose a container_image selector), an explicit spec row would lock the current answer and flag drift. Worth knowing, not blocking.

🤖

🤖 This review was automatically generated with Coder Agents.


# --- IDE modules ---

module "code-server" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 [CRF-4] The base embeds catalog-named module blocks, so selecting the same module in the wizard produces a duplicate module call and a template that fails to build (Hisoka P1, Meruem P1, Pariston P1, Luffy P1, Mafuuu P1, Bisky P2).

The base declares module "code-server", "cursor", "zed", "windsurf", and "git-clone" (lines 273, 282, 304, 313, 324), each name identical to a catalog module ID under coderd/templatebuilder/modules/. Compose renders the base into main.tf and selected modules into modules.tf in the same directory; validateModules (compose.go:203-231) only dedupes within the requested list and never inspects what the base declares. Multiple reviewers reproduced it by running Compose(BaseTemplateID: "quickstart", Modules: [{ID: "code-server"}]): both files emit module "code-server", and Terraform rejects the duplicate.

Verified in a worktree that quickstart is the only base embedding catalog-named modules (siblings declare at most azure_region/gcp_region), so this PR introduces the hazard. The recommended, first-position base breaks on the wizard's core action (add an IDE). This is open question #1, but with no ticket it is an open finding, not a deferral.

Two durable directions: strip the modules so the base is pure infra (option a), or add a base/module collision guard, e.g. declare included_modules in base.json and seed validateModules' seen-map from it so the disjoint-namespace invariant is enforced for every base. A human decides; the reviewer cannot accept this gap as permanent.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and not deferring silently. This P1 — together with CRF-5, CRF-6, and CRF-10 — stems from one root: this is a batteries-included template sitting in the base slot, where the builder's model is base (infra) + modules composed on top.

The resolution is the scope decision tracked as open question #1: either (a) strip the modules/presets so the base is pure infra (which dissolves CRF-4/5/6/10 together), or (b) add a base/module collision guard — e.g. declare included_modules in base.json and seed validateModules' seen-map from it. Option (b) changes shared compose logic and is an architectural call.

I won't preempt that decision or ship a first-position base that breaks on the wizard's core action (adding an IDE), so I've escalated it to the scope owner. Once it's decided I'll implement the chosen option and re-request review. The PR stays draft / do-not-merge until then. (The independent CRF-9 total-order fix landed in 76aabd3.)

count = contains(local.ides, "jetbrains") ? 1 : 0
name = "jetbrains_ides"
display_name = "JetBrains IDEs"
description = "Select the JetBrains IDEs to install"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [CRF-5] The JetBrains IDE selector, preset, and README all promise installation, but the module that would do it is commented out, so the selection is a silent no-op (Leorio P2, Hisoka Note).

The ides parameter offers "JetBrains IDEs"; picking it renders a jetbrains_ides parameter described as "Select the JetBrains IDEs to install", the Backend (Go) preset ships jetbrains, and README.md:68 says JetBrains IDEs are "Filtered by your language selection ... and installed." But the module "jetbrains" that consumes the selection is commented out (its source at line 297 is disabled), and local.jetbrains_selected has no other consumer. The user picks GoLand, the build goes green, and nothing installs, with no error or warning.

Ported verbatim from the example, but it ships as the recommended first base now. Either restore the module, remove the JetBrains option from ides and the README bullet, or (floor, not goal) change the description to state the selection has no effect.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subsumed by CRF-4 / open question #1. If the scope decision is (a) strip to infra, the JetBrains selector, the Backend (Go) preset, and the README bullet all go with it. If IDEs stay, I'll fix the dead path explicitly — either restore the jetbrains module or remove the option and the README claim. Holding rather than editing content that may be removed wholesale.


module "code-server" {
count = data.coder_workspace.me.start_count * (contains(local.ides, "code-server") ? 1 : 0)
source = "registry.coder.com/coder/code-server/coder"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [CRF-6] The base's bundled module sources hardcode registry.coder.com, ignoring the deployment's configured TemplateBuilder.RegistryURL that every builder-composed module honors (Hisoka).

Builder-composed modules are templated with {{ .RegistryBase }}, fed from api.DeploymentValues.TemplateBuilder.RegistryURL, so air-gapped or private-mirror deployments can redirect module pulls. The quickstart base carries no Go-template directives (why the golden is byte-identical), so its source = "registry.coder.com/coder/code-server/coder" lines are frozen. On a deployment with a custom RegistryURL, the base modules still reach for the public registry and terraform init fails when it is unreachable.

The runtime failure is unverified (no air-gapped sandbox), but the hardcoding and the divergence from the builder's own registry contract are confirmed in the diff. Stripping the modules (open question #1 option a) also removes this divergence.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also subsumed by CRF-4 / open question #1. Stripping the bundled modules (option a) removes the hardcoded registry.coder.com sources entirely. If the modules stay, I'll route their sources through the builder's RegistryBase so they honor TemplateBuilder.RegistryURL like composed modules do. Holding for the scope decision.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [CRF-6] Re-raising. The IDE module hardcodes were removed, but git-clone still hardcodes source = "registry.coder.com/coder/git-clone/coder" (main.tf.tmpl:157), and this is structural (Mafuuu P2, Melody P2, Knov P2, Meruem P2, Chopper P2, Luffy P2; Pariston P3; Hisoka/Zoro Note).

Verified in a worktree: BaseRenderContext (render.go:19-23) has only ContainerImage, ImageOptions, Variables, with no RegistryBase field, and renderBase (compose.go:112) is never passed req.RegistryURL. Only ModuleRenderContext carries RegistryBase (fed at compose.go:270). So the base cannot honor RegistryURL, and the same git-clone module resolves through the mirror when the wizard renders it but reaches the public registry when the base embeds it, on the same deployment. When git_repo is set, terraform init on a mirror/air-gapped deployment pulls the base git-clone from an unreachable registry and the build fails; the wizard also hides git-clone for this base (it is in included_modules), so there is no non-broken path to build-time clone there.

Context for the human decision: this base is unusable fully air-gapped regardless (image, apt, nodesource, go.dev, rustup), so the value is mirror consistency, and three shipped bases already hardcode region-module sources, so this is a base-layer class. No ticket exists, so this needs a human call: thread RegistryBase into base rendering, drop git-clone from the base and let the wizard provide it, or track it.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope owner's call: keep git-clone in the base and take option (c) here (accept + track), with the real fix stacked so this PR stays tight. 658b74b adds an in-code note that the base renders its module source verbatim against the public registry and does not yet honor a deployment's registry mirror. Threading RegistryBase into base rendering is tracked in DOCS-610 and lands as a PR stacked on this branch. As you noted, this base isn't fully air-gap-usable regardless, and three shipped bases already hardcode region-module sources, so we're not blocking the base PR on it.

@@ -0,0 +1,437 @@
terraform {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-10] The base template is a byte-for-byte copy of the already-embedded examples/templates/quickstart/main.tf, so the binary carries two identical copies with nothing to keep them aligned (Robin).

The example is embedded via //go:embed templates/quickstart and the base via //go:embed bases; diff reports all 437 lines identical. Every other base diverges from its example on purpose (trimmed to infra), so only this one is a verbatim clone. coderd/templatebuilder imports nothing from examples and no test compares the two, so editing the example (a module bump, a prerequisites edit) silently drifts the wizard's Quickstart from the shipped one. The same applies to install-languages.sh.tftpl and README.md. Resolving open question #1 by trimming to infra makes the copies legitimately different; keeping verbatim needs a test asserting the base equals the embedded example.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same root as CRF-4 (open question #1). Trimming to infra makes the base legitimately diverge from the embedded example. If we instead keep it verbatim, I'll add a test asserting the base equals examples/templates/quickstart so the two can't silently drift. Holding for the scope decision.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-10] Re-raising. Trimming main.tf.tmpl resolved the copy for that file, but the drift class survived: bases/quickstart/install-languages.sh.tftpl is byte-for-byte identical to examples/templates/quickstart/install-languages.sh.tftpl (diff reports identical), and both are consumed via templatefile(...) (Robin).

No test asserts the two stay equal, so adding a language to one silently leaves the other installing the old set, and the example and the builder-rendered workspace diverge under the same "Coder Quickstart" name. Either embed a shared source, or add a test that fails when the base file and the example file diverge.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushing back on coupling the files. After 658b74b the base's install-languages.sh.tftpl is no longer byte-identical to the example (the dispatch was rewritten to has_language), so that coupling is already broken. The base is intentionally its own artifact now (main.tf.tmpl was trimmed to diverge), so a test asserting base == examples/templates/quickstart would wrongly block legitimate divergence. The real drift risk (a base's rendered modules vs its manifest) is covered by the new CRF-12 invariant test. If you'd rather share a single embedded source across both surfaces, that's a larger refactor than this base PR and worth its own change.

"id": "quickstart",
"display_name": "Coder Quickstart",
"os": "linux",
"default_context": {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-7] The container image is hardcoded (codercom/enterprise-base:ubuntu in main.tf.tmpl) while sibling bases like docker expose a container_image builder variable (Mafuuu, Luffy).

base.json declares no variables, so the flagship, first-position base is the one base a user cannot re-image without editing generated Terraform. This is the author's open question #3; noting it so the inconsistency is a conscious decision, and it is worth deciding alongside #1.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — this is open question #3. The image is hardcoded to stay faithful to the ported example; I'll decide alongside open question #1 whether the flagship base should expose a container_image variable like the docker base does.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decision: accept, keep the image hardcoded. The quickstart base is the opinionated starter; the Docker base is the bring-your-own-image path, and exposing container_image here blurs that line. The baseSpec row locks quickstart to no builder variables, so adding one later is a deliberate change. Tracked in DOCS-611 to revisit on user demand.

Comment thread coderd/templatebuilder_handler.go Outdated
})
}

// The Coder Quickstart base is surfaced first as the recommended starting

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-8] Quickstart-first ordering is a hardcoded ID pin inside the comparator rather than data (Pariston, Meruem).

The comparator sorts every base by Name but special-cases one base by ID. It works and is proportional for a single pinned base (open question #2). If a second pinned base ever appears, the comparator stops expressing intent and an order field in base.json would move the policy back to data. Recording so the decision is explicit; no change needed now.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — open question #2 (placement pending sign-off). The single-ID pin is intentional for now; if a second pinned base ever appears I'll move the policy into an order field in base.json rather than special-casing IDs in the comparator. No change now.

Comment thread coderd/templatebuilder_handler.go Outdated
@@ -0,0 +1,437 @@
terraform {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-3] The golden is byte-for-byte identical to the embedded main.tf.tmpl because the ported template has no Go-template directives, so the snapshot asserts source == source (Netero, Law).

Low incremental value, but not vacuous: it guards against accidental corruption of the embedded file and confirms RenderBaseTemplate handles directive-free input. This is a third copy of the same 437 lines in the tree (see CRF-10). No change required; flagged for awareness.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — the golden is identical to the embedded template because the port carries no Go-template directives, and it still guards against accidental corruption of the embedded file. Tied to CRF-10; I'll revisit it with the scope decision.

@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 3 status: review is on hold. The churn guard classified the prior findings and the panel did not re-run this round, because the code is essentially unchanged (only CRF-9 landed) and the substantive findings are waiting on a decision that has not happened yet.

What moved:

  • CRF-9 (unstable/non-total sort): fixed in 76aabd3. Confirmed in a worktree: the comparator now tiebreaks on ID (if bases[i].Name != bases[j].Name { ... } return bases[i].ID < bases[j].ID), so the order is total and deterministic even if two bases share a display name. Closed. Switching to slices.SortFunc + cmp.Compare remains optional.

Held pending the open-question-#1 scope decision (escalated to the scope owner):

  • CRF-4 (P1, module collision), CRF-5 (P2, dead JetBrains selector), CRF-6 (P2, hardcoded registry), CRF-10 (P3, verbatim-copy drift). You confirmed these share one root and committed to a concrete fix under either option (a) strip to infra or (b) collision guard. That is the right call, and not shipping a first-position base that breaks on the wizard's core action is correct. These stay open findings, not resolved, until the chosen option is implemented or a tracking ticket is filed. A human decision is what unblocks them.

Acknowledged with reasoning, no action expected now:

  • CRF-7 (container image, open q#3), CRF-8 (ID pin in comparator, open q#2), CRF-3 (golden equals source).

One finding is unaddressed with no response and no code change:

  • CRF-11 (Note): quickstart is absent from the handler-level specs table in coderd/templatebuilder_handler_test.go, so its API surface (currently no variables) is only checked by generic loops. It was folded into the round-2 review body, so there is no thread to reply on. Either add a specs row for quickstart or say why it should not be locked. This is minor, but it is the one item with no engagement.

The round-2 CHANGES_REQUESTED still stands. The panel will re-review once you push the open-question-#1 implementation (or file a ticket for it) and address or respond to CRF-11. Re-request review then.

🤖 This review was automatically generated with Coder Agents.

@nickvigilante

Copy link
Copy Markdown
Contributor Author

Re CRF-11 (quickstart absent from the handler specs table): addressed in b64ceea.

Added a quickstart row to the baseSpec table in TestTemplateBuilderBases/OK asserting its OS and that it currently exposes no builder variables (hasVariables: false), matching the existing aws-linux precedent. Builder variables come from base.json's variables array, which quickstart doesn't declare, so this locks the current answer and flags drift if open question #3 later adds a container_image selector (the row's expectedVars would be updated alongside that base.json change).

This was the one finding folded into the round-2 body with no inline thread, hence this top-level reply rather than a thread response.

@nickvigilante
nickvigilante marked this pull request as ready for review July 15, 2026 18:31
@coder-tasks

coder-tasks Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Documentation Check

The template builder is enabled by default and its base-selection step is documented in docs/admin/templates/creating-templates.md. This PR adds a new Coder Quickstart base to that step. As of 17186ff, it is no longer pinned first as the recommended starting point; it is now grouped immediately before the Docker base (it's a Docker-based starter).

Note

The PR description still flags template scope as pending sign-off and remains a draft. If the base's contents or placement change again, revisit these items.

Updates Needed

  • docs/admin/templates/creating-templates.md - note the Quickstart base pinned first as the recommended starting point (no longer applicable: 17186ff changed the ordering so Quickstart is grouped next to Docker rather than pinned first as "recommended"; the base-selection step describes bases with an illustrative examples list and does not enumerate them, so adding one more base does not require a prose change)
  • docs/images/templatebuilder_01_bases.png - The base-selection screenshot predates this change and does not show the new Coder Quickstart base (now positioned just before Docker). Refresh it once the base's contents are finalized so the screenshot matches the base list in the UI.

Note: the base's own README.md (coderd/templatebuilder/bases/quickstart/README.md) is embedded template content surfaced in the builder, not published under docs/, so it isn't part of the docs site.


Automated review via Coder Agents

@david-fraley david-fraley left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We chatted, follow ups for you -- move it to be next to the Docker template and also remove the IDE selector

@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full panel review (round 4). The scope decision landed and was implemented well. The base is trimmed to infra plus a language selector and optional git-clone, the dead IDE selector is gone (CRF-5), the ordering is a clean group-before-Docker with a total-order comparator (CRF-8/CRF-9), the baseSpec row is in (CRF-11), and the collision guard is real: it rejects a base-included module in compose and hides it from the modules endpoint, with tests on both paths. Verified in a worktree. CRF-4's git-clone instance is genuinely fixed. As Robin put it, "You found the seam and cut it. But the poneglyph next to it still has an exact twin."

Severity count: 2 P2, 5 P3, 1 Note, 1 Nit. Two prior findings re-raised (CRF-6, CRF-10).

The two P2s both trace to one decision: keeping a catalog module (git-clone) embedded in the base.

  • CRF-6 (re-raised, six reviewers): the base's git-clone still hardcodes registry.coder.com, and this is structural, not a stray literal. BaseRenderContext has no RegistryBase field and renderBase is never passed the deployment RegistryURL, so a base cannot honor CODER_TEMPLATE_BUILDER_REGISTRY_URL the way wizard-composed modules do. The same git-clone module resolves through the mirror when the wizard renders it but reaches the public registry when the base embeds it, on the same deployment. Fair context from Hisoka and Knov: this base is unusable fully air-gapped regardless (it pulls the enterprise image, apt, nodesource, go.dev, rustup), so the real value here is mirror consistency on internet-connected deployments, and three shipped bases already hardcode region-module sources, so this is a base-layer class. It still needs a human decision (no ticket): thread RegistryBase into base rendering, drop git-clone from the base and let the wizard provide it (which also removes the guard's need for git-clone, per Knov/Melody/Pariston), or track it.
  • CRF-13 (two reviewers): the README promises languages are "installed on first start and cached for subsequent starts," but the installs land in the ephemeral container (only /home/coder persists), so they reinstall from the network on every start, and start_blocks_login = true holds the user's login during that reinstall. On mirror/air-gapped deployments the reinstall fails and login never unblocks. Fix the doc to match the behavior, or persist the toolchains.

The collision guard works but its invariant is not enforced (CRF-12, eight reviewers): included_modules is a hand-maintained copy of the base's actual module blocks with nothing binding them. Add a module "code-server" block to any base and forget the JSON line, and CRF-4's P1 duplicate-module break returns silently. The fix addressed the git-clone instance; the durable fix is mechanical, a test that parses each base's rendered module "<id>" labels and asserts they equal that base's included_modules (or derive the set from the template and delete the field). AGENTS.md prefers mechanical enforcement over a remembered rule, and this guard exists to prevent a P1.

Smaller items: CRF-10 re-raised, the drift class survived the trim in install-languages.sh.tftpl (still byte-identical to the example); CRF-14 unanchored grep language dispatch (safe today, latent); CRF-15 a vacuous negative test loop; CRF-16 the collision rationale is duplicated ~6 times across the diff (also at bases.go:268); CRF-17 the "bidirectional" conflict comment overpromises for base-included modules; CRF-18 stdlib modernization for the comparator/regroup. No action needed on CRF-3 (the directive-free golden still asserts source==source; all reviewers concur it is low-value but not vacuous). CRF-7 (container image) remains deferred; the scope owner did not require it, but there is no ticket, so it needs an explicit accept-or-track. Luffy also noted two of the four presets ("Backend (Go)", "Data Science") select a single language the dropdown already offers.


coderd/templatebuilder/compose.go:236

Note [CRF-17] The "Check conflicts bidirectionally" loop iterates only requested modules, so a base-included module's own ConflictsWith is never enforced (Mafuuu, Melody).

Base module IDs are seeded into seen, so a requested module that declares a conflict with a base module is caught, but the base module's own ConflictsWith list is never consulted. If a base ever includes module A where A declares a conflict with B but B does not reciprocate, selecting B composes cleanly. Latent today (git-clone's conflicts_with is empty and catalog conflicts appear symmetric); flagged because the comment claims a bidirectionality that does not extend to base-included modules.

🤖

🤖 This review was automatically generated with Coder Agents.

"display_name": "Coder Quickstart",
"os": "linux",
"default_context": {},
"included_modules": ["git-clone"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-12] The collision guard's invariant is not enforced: included_modules is a hand-maintained duplicate of the base's actual module blocks, and nothing binds them (Hisoka, Mafuuu, Pariston, Chopper, Melody, Meruem, Zoro P3; Knov P2).

Today included_modules: ["git-clone"] matches the single module "git-clone" block. But the two live in separate files with no link, and no test cross-checks them (the only reference is BaseExcludesIncludedModules, which asserts the endpoint honors the JSON, not that the JSON describes reality). If a future edit adds a catalog-named module block and forgets the JSON line (or renames the block), validateModules stops reserving the name and the endpoint starts offering it, and CRF-4's P1 duplicate-module break returns with no failing test. There is also no check that included_modules entries are real catalog IDs, so a typo silently protects the wrong name.

The R4 fix closed the git-clone instance; this closes the class. Mechanical fix (AGENTS.md prefers this): a test that renders each base, scans for module "<id>" labels that are catalog IDs, and asserts the set equals that base's included_modules, or derive the set from the rendered base (the package already parses base HCL in ExtractAgentResourceName) and delete the field.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enforced in 657abbb. Added ExtractModuleNames and TestBaseIncludedModulesMatchRendered: it renders each base, keeps the module "<id>" labels that are catalog IDs, and asserts that set equals the base's included_modules. A base that adds a catalog-named module block without the manifest line (or lists a phantom/typo'd one) now fails. Verified empirically: it fails when I temporarily emptied quickstart's included_modules, and passes once restored.

- **Docker container** (ephemeral) running Ubuntu with the Coder agent
- **Docker volume** (persistent) mounted at `/home/coder`

Files in your home directory persist across workspace restarts. Selected languages are installed on first start and cached for subsequent starts.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [CRF-13] The README promises languages are "installed on first start and cached for subsequent starts," but nothing caches them (Leorio P2, Meruem P2).

The only persistent storage is the home_volume at /home/coder; the container is recreated from codercom/enterprise-base:ubuntu on every start. install-languages.sh.tftpl installs Node/Java/C++ via apt into /usr and Go into /usr/local/go, none of which persist, and its command -v idempotency guard fails after each rebuild, so every start reinstalls from the network (only Rust's ~/.cargo persists). Because the script is run_on_start = true with start_blocks_login = true, each restart blocks login on a full reinstall, and on mirror/air-gapped deployments (see CRF-6) that reinstall fails and login never unblocks.

Fix the doc to state that toolchains reinstall on each start, or change the design to install into the persistent volume / a prebuilt image. The current sentence is a false promise on the recommended starter.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 658b74b. The README now states the toolchains reinstall on every start (most install into the ephemeral container; only Rust's ~/.cargo persists) and that the start script blocks login until it finishes. Persisting toolchains / a prebuilt image is a separate design change, out of scope for this base PR.

fi
}

if echo "$LANGUAGES" | grep -q "python"; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-14] Language dispatch uses an unanchored substring match, so it is correct only because the current values happen not to overlap (Meruem P3; Hisoka, Mafuuu, Knov Note).

Each block tests the comma-joined selection with echo "$LANGUAGES" | grep -q "<token>" (e.g. grep -q "go"). The six values (python, nodejs, go, rust, java, cpp) are substring-disjoint today, so it works, but any future value that contains an existing token (golang, mongo, django) would silently trigger the wrong install branch with no error. Match tokens exactly, e.g. case ",$LANGUAGES," in *,go,*) ... ;;, so the installer is not coupled to option names never overlapping.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 658b74b using exactly the idiom you suggested: a has_language helper wrapping case ",$LANGUAGES," in *",$1,"*). Verified LANGUAGES=python,nodejs,mongodb no longer triggers the go branch, while python,go does.

// docker base does not bundle it, so it stays available there.
quickstartResp, err := client.TemplateBuilderModules(ctx, "quickstart")
require.NoError(t, err)
for _, m := range quickstartResp.Modules {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-15] The quickstart side of BaseExcludesIncludedModules is a vacuous negative loop that passes on an empty list (Bisky).

for _, m := range quickstartResp.Modules { require.NotEqual(t, "git-clone", m.ID, ...) } asserts nothing if quickstartResp.Modules is empty, so if the filter ever over-filters to an empty list the test still goes green and reports the guard as working. The docker half is honest (require.True(t, dockerHasGitClone)). Add require.NotEmpty(t, quickstartResp.Modules) before the loop so it proves the filter kept the other modules, matching the sibling subtest's pattern.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 657abbb. Added require.NotEmpty(t, quickstartResp.Modules) before the loop, so the test proves the filter kept the other modules instead of passing on an empty list.

Comment thread coderd/templatebuilder_handler.go Outdated
})
}

// Order bases alphabetically by display name, then group the Coder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-16] The base/module collision rationale is written about six times across this diff, and the comments narrate the code rather than explain it (Gon, who rated the two clearest instances P2).

The rationale appears at bases.go:47, bases.go:268, compose.go:202, compose.go:209, handler.go:87, and handler.go:131. The inline comment here narrates the sort.Slice and groupQuickstartBeforeDocker statements it precedes and repeats the placement rationale that the function's own doc (:131) already carries; the BaseIncludedModules doc (bases.go:268) restates the field doc and describes Compose's behavior. Duplicated rationale drifts apart on the next edit. Keep the reason at the check site (validateModules) and the data source (the field); drop the narration at the pass-through sites.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1cf64f1. Kept the rationale at the check site (validateModules) and the data source (BaseManifest.IncludedModules), and reduced the pass-through sites (the BaseIncludedModules doc and the modules-endpoint comment) to short pointers. The inline ordering narration is gone: the sort is now a single slices.SortFunc.

Comment thread coderd/templatebuilder_handler.go Outdated
// Quickstart base directly before the Docker base. Quickstart is a
// Docker-based "start here" template, so it belongs next to Docker rather
// than in its default alphabetical slot.
sort.Slice(bases, func(i, j int) bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-18] Stdlib modernization for the ordering code (go 1.26) (ging-go, Robin).

The comparator is sort.Slice with a reflection-based less func; slices.SortFunc(bases, func(a, b) int { return cmp.Or(cmp.Compare(a.Name, b.Name), cmp.Compare(a.ID, b.ID)) }) is the idiom and drops the sort import (behavior identical, comparator already total). In groupQuickstartBeforeDocker, the manual dockerIdx search is slices.IndexFunc and the three-append splice is slices.Insert(rest, dockerIdx, quickstart). Style/type-safety only; take it or leave it.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1cf64f1. The comparator is now slices.SortFunc(bases, func(a, b) int { return cmp.Or(cmp.Compare(a.Name, b.Name), cmp.Compare(a.ID, b.ID)) }), the sort import is dropped, and groupQuickstartBeforeDocker uses slices.IndexFunc + slices.Insert. Behavior identical (TestTemplateBuilderBases green).

@github-actions github-actions Bot added the stale This issue is like stale bread. label Aug 1, 2026
@github-actions github-actions Bot closed this Aug 4, 2026
@nickvigilante nickvigilante reopened this Aug 4, 2026
@nickvigilante
nickvigilante marked this pull request as draft August 4, 2026 16:01
@nickvigilante
nickvigilante force-pushed the vigilante/docs-558-add-coder-quickstart-template-to-template-builder branch from 30fb31b to 658b74b Compare August 4, 2026 18:01
@nickvigilante

Copy link
Copy Markdown
Contributor Author

R4 findings are answered in each thread and pushed as 657abbb, 1cf64f1, 658b74b.

One finding had no inline thread (it was folded into the review body):

CRF-17 (conflict-check comment): fixed in 1cf64f1. The comment now states that base-included modules seed the seen-set (so a requested module conflicting with one is caught), but a base module's own ConflictsWith list is not consulted, which is safe because base modules are curated.

For the round: CRF-12/15 (657abbb), CRF-13/14 and the CRF-6 note (658b74b), and CRF-16/17/18 (1cf64f1) are all code changes; CRF-6 and CRF-7 are accept-and-track (DOCS-610 with a stacked follow-up PR, and DOCS-611); CRF-10 is a push-back (details in-thread).

Validation on the touched packages: go test ./coderd/ ./coderd/templatebuilder/ including -race -shuffle=on, plus gofmt, go vet, terraform fmt, typos, and make lint/emdash, all clean.

@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full panel review (round 5), a verification round. This is in good shape: all seven R4 fixes hold up under the panel's checks, and I'm dismissing the prior CHANGES_REQUESTED because no blocking (P0-P2) findings remain.

Verified fixed: CRF-12 (the invariant test TestBaseIncludedModulesMatchRendered renders each base, extracts module "<id>" labels via ExtractModuleNames, and asserts they equal included_modules, so the manifest can no longer silently drift, this closes the class, not the instance), CRF-13 (README no longer promises caching), CRF-14 (has_language whole-entry match), CRF-15 (require.NotEmpty), CRF-16 (comment dedup), CRF-17 (honest ConflictsWith comment), CRF-18 (slices.SortFunc/IndexFunc/Insert). As Bisky put it, "Costume jewelry does not survive this cut."

Contested finding closed in the author's favor: CRF-10 (base install-languages.sh drift). Eight reviewers independently diffed the base against the example and confirmed the base has genuinely diverged (base uses has_language, the example still uses grep -q), so a base==example test would wrongly block legitimate divergence, and the real manifest-vs-rendered drift is now covered by CRF-12. Defense valid.

Deferred/accepted with tickets, not re-raised: CRF-6 (git-clone registry, DOCS-610 with a stacked follow-up PR) and CRF-7 (container image, DOCS-611). The in-code CRF-6 note was verified accurate.

Remaining items are all P3 or below, none blocking:

  • CRF-20 (P3): the language selector's option values and the install script's has_language branches are the same kind of hand-maintained two-list pairing you just made mechanical for included_modules, but with no cross-check test. Verified: six values, six branches, they agree today, nothing binds them. Drift silently no-ops a selected language (the CRF-5 shape). The same ExtractModuleNames-style test would close it.
  • CRF-23 (P3): CRF-13's doc fix is honest, but the behavior it now documents (every start reinstalls toolchains from the network and blocks login) is a real limitation on network-restricted deployments with no ticket. DOCS-610 covers the git-clone registry, not this. Per the no-follow-up rule, confirm it is tracked or explicitly accepted.
  • CRF-21 (Note): the CRF-12 invariant test renders with DefaultBaseRenderContext only, but production renderBase overlays merged variables, so a base that gated a catalog module block on a variable would evade the guard. Dormant today (quickstart declares no variables), worth a comment or a variable-context case if bases ever gain variables.
  • CRF-22 (Note): the README's leading "reinstalled on every start" is not literally true for Rust ($HOME/.cargo persists); the trailing "most" hedges it, and Leorio (docs) considers the current wording acceptable. Optional refinement.
  • CRF-24 (Nit): the sort-site comment still repeats the groupQuickstartBeforeDocker rationale that the helper's own doc carries (a residual of the CRF-16 dedup).

Out of this PR's scope but worth a follow-up: the source example examples/templates/quickstart/install-languages.sh.tftpl still carries the old unanchored-grep substring dispatch that CRF-14 fixed in the base (flagged by Mafu-san, Hisoka, Razor). Port has_language to the example in its own change. ging-go also noted the adjacency loop in the Sorted test could be slices.IsSorted (loses the per-pair failure message, so optional).

🤖 This review was automatically generated with Coder Agents.

icon = "/icon/code.svg"
order = 1

option {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-20] The language selector's option values and the install script's has_language branches are hand-maintained duplicates across two files with no cross-check test (Bisky).

The coder_parameter "languages" block offers six values (python, nodejs, go, rust, java, cpp) and install-languages.sh.tftpl has six matching has_language branches. Verified they agree today, and verified nothing binds them (no test references both). Add an option value and forget the branch, or rename cpp to c++ on one side, and the user selects a language, the workspace starts, and nothing installs, no error. That is the CRF-5 silent-no-op shape, and the CRF-12 hand-synced-lists shape you just made mechanical for included_modules. Close it the same way: render the base, extract the coder_parameter "languages" option values and the has_language arguments, and require.ElementsMatch the two sets. Rationed to P3 because the code is correct today and the fix is one test.

🤖

# All languages install in a single script to avoid apt-get lock
# conflicts (coder_script resources run in parallel).

resource "coder_script" "install_languages" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-23] CRF-13's doc fix is honest, but the behavioral residual it documents is untracked (Mafu-san).

The README now correctly says languages reinstall on every start and block login. The behavior behind it: coder_script.install_languages runs with run_on_start = true and start_blocks_login = true, and the script fetches every toolchain over the network (nodesource, go.dev, rustup, apt). On a network-restricted or air-gapped deployment, every workspace start blocks login on an install that cannot complete. That is a distinct mechanism from CRF-6/DOCS-610 (the git-clone registry mirror), so DOCS-610 does not cover it. Per the no-follow-up rule an untracked residual is a human decision, not an implicit accept: confirm it is tracked (a ticket) or explicitly accept the limitation. The doc-fix itself is fine.

🤖

t.Parallel()

mainTF, err := templatebuilder.RenderBaseTemplate(
id, "main.tf.tmpl", templatebuilder.DefaultBaseRenderContext(id))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-21] The CRF-12 invariant test renders each base with DefaultBaseRenderContext only, but production renderBase overlays merged user variables (Hisoka).

So the guard verifies the manifest-vs-rendered mirror under a context production may not use. A base whose main.tf.tmpl gated a module "<catalog-id>" block on a variable ({{ if .Variables.x }}module "code-server"{{ end }}) would render that block in production while the test, running defaults, never sees it, and included_modules would not need to list it to pass, re-opening CRF-4 with the test green. Dormant today: the only base with included_modules is quickstart, which declares zero variables and is directive-free. Worth a note in the test or a variable-context case if bases ever gain variables.

🤖

- **Docker container** (ephemeral) running Ubuntu with the Coder agent
- **Docker volume** (persistent) mounted at `/home/coder`

Files in your home directory (`/home/coder`) persist across workspace restarts. The selected languages are reinstalled on every start by a script that blocks login until it finishes: most language tools install into the workspace container rather than your home directory, so they do not persist and are fetched from the network each time the workspace starts.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-22] The CRF-13 fix's leading clause slightly over-corrects: "reinstalled on every start" is not literally true for Rust (Mafuuu; Leorio and Hisoka consider the current wording acceptable).

Rust installs to $HOME/.cargo on the persistent home volume and is skipped on restart by the [ -f "$HOME/.cargo/bin/rustc" ] guard; every other language installs into the ephemeral container and does reinstall. The trailing "most language tools ... do not persist" hedges the aggregate correctly, so this is a minor optional refinement (e.g. "most selected languages are reinstalled on every start"), not a false promise.

🤖

Comment thread coderd/templatebuilder_handler.go Outdated
sort.Slice(bases, func(i, j int) bool {
return bases[i].Name < bases[j].Name
// Order bases alphabetically by display name, then group the Coder
// Quickstart base directly before the Docker base. Quickstart is a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-24] Residual of the CRF-16 dedup: the sort-site comment still repeats the placement rationale that groupQuickstartBeforeDocker's own doc carries (Gon).

The CRF-16 fix cut the collision rationale from ~6 copies to a canonical site; this ordering rationale is the one remaining verbatim pair (sort call site + helper doc). The helper owns the reordering and its why, so the sort-site comment can drop the rationale and keep just what the sort does, e.g. "Sort by display name, then regroup Quickstart next to Docker (see groupQuickstartBeforeDocker)."

🤖

Port the existing `quickstart` example template into the template builder's
base catalog so it can be selected in the guided wizard, and surface it first
in the base list as the recommended starting point.

- Add coderd/templatebuilder/bases/quickstart (base.json, README with
  prerequisite markers, main.tf.tmpl, install-languages.sh.tftpl), ported
  as-is from examples/templates/quickstart.
- Pin the quickstart base first in the bases endpoint; other bases stay
  alphabetical.
- Extend base ID/OS/snapshot tests and add the rendered golden file.

Scaffold for DOCS-558. First-position placement and the template's
scope/contents are still pending sign-off; see the PR description.
The quickstart-first pin changed the bases ordering contract, which broke
the existing TestTemplateBuilderBases/Sorted assertion (strict alphabetical
across all bases). Update Sorted to encode the new contract: quickstart is
pinned first, and the remaining bases stay sorted by name.

Addresses coder-agents-review CRF-1 (P0) and the missing positive-ordering
coverage noted in CRF-2.
Tiebreak the bases comparator on ID when display names are equal, so the
sort is total and deterministic regardless of sort.Slice's instability.

Addresses coder-agents-review CRF-9.
Quickstart was absent from the curated baseSpec table in
TestTemplateBuilderBases/OK, so its API surface (name, icon, OS, and
variable presence) was only exercised by the generic response loops.
Add an explicit row asserting it exposes no builder variables today,
locking the current answer so drift (e.g. later exposing a
container_image selector) is flagged.
Addresses review feedback on the Coder Quickstart base:

- Remove the IDE selector: drop the ides parameter, the dependent
  jetbrains_ides parameter, the IDE locals, and the IDE modules
  (code-server, cursor, jetbrains, zed, windsurf); strip the ides/
  jetbrains_ides keys from the presets; and update the README, which no
  longer advertises editor selection. Editors are added via the builder's
  module step instead. This also removes the dead JetBrains selector and
  the IDE base/module name collisions.
- Placement: sort bases alphabetically by name and group the quickstart
  base immediately before the Docker base instead of pinning it first.

Regenerate the quickstart golden and update the Sorted test for the new
ordering.
Keep the quickstart base's git-clone module (build-time repo cloning is a
useful quickstart affordance) but stop it from colliding with the wizard's
own Git Clone module.

- Add an optional included_modules field to base.json (BaseManifest)
  listing the catalog module IDs a base already declares in its own
  Terraform. Quickstart declares ["git-clone"].
- Seed validateModules' seen-set with those IDs so a wizard-selected
  module the base already renders is rejected with a clear error,
  enforcing a disjoint base/module namespace for every base.
- Filter GET /templatebuilder/modules?base=<id> to omit the base's
  included modules, so the wizard never offers a colliding module.

Tests: compose rejects quickstart + git-clone and still allows a
non-included module; the modules endpoint excludes git-clone for
quickstart while keeping it for docker.
Add ExtractModuleNames and a per-base test asserting each base's
included_modules manifest field exactly lists the catalog modules it
renders. Without this, adding a `module "<catalog-id>"` block to a base
and forgetting the manifest line silently re-opens the duplicate-module
collision the guard exists to prevent.

Also assert the quickstart modules endpoint returns a non-empty list, so
the git-clone exclusion test can no longer pass vacuously.

Addresses CRF-12 and CRF-15.
Replace the hand-rolled base sort and quickstart/docker regroup with
slices.SortFunc + cmp.Or and slices.IndexFunc/Insert. Behavior is
unchanged (verified by TestTemplateBuilderBases).

Consolidate the base/module collision rationale onto
BaseManifest.IncludedModules and point the other sites at it instead of
re-explaining it. Correct the conflict-check comment: base-included
modules seed the seen-set, but a base module's own ConflictsWith is not
consulted.

Addresses CRF-16, CRF-17, and CRF-18.
Correct the README claim that languages are cached across starts: most
tools install into the ephemeral workspace container, so they reinstall
from the network on every start and block login until done.

Match selected languages against whole comma-separated entries instead
of an unanchored grep substring, so a future value can never partially
match another.

Note in the base that its git-clone module renders the public registry
verbatim and does not yet honor a deployment's registry mirror; threading
the registry through base rendering is tracked in DOCS-610 as a stacked
follow-up. Regenerate the quickstart golden for the added comment.

Addresses CRF-13, CRF-14, and CRF-6.
- CRF-20: bind the quickstart language selector to the install script.
  Add ExtractParameterOptionValues and a test asserting the selector's
  option values match the script's has_language branches, so the two
  hand-maintained lists cannot drift silently.
- CRF-21: document that TestBaseIncludedModulesMatchRendered renders with
  DefaultBaseRenderContext (no variable overlay) and to extend it if bases
  gain variables.
- CRF-22: make the README precise that Rust's ~/.cargo persists while other
  toolchains reinstall each start.
- CRF-24: drop the duplicated grouping rationale at the sort site (kept on
  groupQuickstartBeforeDocker).
@nickvigilante
nickvigilante force-pushed the vigilante/docs-558-add-coder-quickstart-template-to-template-builder branch from 658b74b to bc740d2 Compare August 4, 2026 19:31

Copy link
Copy Markdown
Contributor Author

R5 findings addressed. All code changes are in bc740d2 (the branch was rebased onto main at push, so the per-thread commit hashes from the last round now live under new SHAs; the changes they referenced are unchanged).

CRF-20 (P3) — fixed in bc740d2. Added ExtractParameterOptionValues and TestQuickstartLanguageSelectorMatchesInstallScript: it renders the base, extracts the languages selector's option values and the script's has_language dispatch names, and asserts the two sets match. Same shape as the included_modules invariant, so these two hand-maintained lists can no longer drift silently. Verified it fails on drift (renaming the script's cpp branch to cxx trips ElementsMatch) and passes again on revert.

CRF-21 (Note) — addressed in bc740d2. Documented on TestBaseIncludedModulesMatchRendered that it renders with DefaultBaseRenderContext (no variable overlay), so a base that gated a catalog module block on a variable could evade the guard, and that the test should gain a variable-context case when bases start declaring variables. Left as a comment rather than a new case because no base declares variables today (dormant), matching your framing.

CRF-22 (Note) — fixed in bc740d2. The README now names Rust as the explicit exception (its ~/.cargo toolchain persists on the home volume and is detected and reused) instead of a blanket "reinstalled on every start," while keeping the point that the other toolchains reinstall from the network each start.

CRF-23 (P3) — tracked in DOCS-614 (Medium). CRF-13's doc fix stands; the underlying limitation (language toolchains reinstall from the network every start, block login, and fail on network-restricted/air-gapped deployments) is now a ticket with possible directions (persist to the home volume, a prebuilt image / prebuilds, or a non-blocking install). Distinct from DOCS-610, which covers the git-clone module registry.

CRF-24 (Nit) — fixed in bc740d2. Dropped the duplicated grouping rationale at the sort site; it's now a short pointer to groupQuickstartBeforeDocker, whose own doc carries the "Docker-based starter, grouped next to Docker" reasoning.

Out of scope

  • The source example examples/templates/quickstart/install-languages.sh.tftpl still carries the pre-CRF-14 unanchored grep dispatch. Tracked in DOCS-613 to port has_language there in its own change, as suggested.
  • slices.IsSorted for the Sorted adjacency loop: keeping the explicit loop so a failure names the specific out-of-order pair, which IsSorted would drop. Declining on that basis.

Validation on the touched packages: go test ./coderd/ ./coderd/templatebuilder/ including -race -shuffle=on, plus gofmt, go vet, and the typos/emdash checks, all clean.

@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full panel review (round 6), verification. All four R5 fixes hold, no blocking findings remain, and I'm dismissing the prior CHANGES_REQUESTED.

Verified closed: CRF-20 (the language-selector-vs-has_language drift guard is real, six reviewers independently mutation-tested it, renaming a script branch or a selector option fails the test loudly), CRF-21 (the default-context caveat on the invariant test is accurate), CRF-22 (the README now names Rust as the persist-and-reuse exception, matching the $HOME/.cargo guard), CRF-24 (the sort-site comment is a one-line pointer). Deferrals with tickets confirmed and not re-raised: CRF-6 (DOCS-610), CRF-7 (DOCS-611), CRF-23 (DOCS-614); the out-of-scope example-grep is tracked (DOCS-613). As Hisoka put it: "Boring, in the way a well-built thing is boring."

One new P3 and one Note, neither blocking:

  • CRF-25 (P3): the same drift class the panel closed with CRF-5 and CRF-20 has a third consumer the new guard does not cover. The four coder_workspace_preset blocks declare languages a third time (web_dev, backend_go, data_science, full_stack), and nothing binds them to the selector. Today all preset values are valid, so it is latent, exactly as CRF-20 was when rated P3. Extend TestQuickstartLanguageSelectorMatchesInstallScript to also assert each preset's decoded languages is a subset of the selector options.
  • CRF-19 (Note): ExtractParameterOptionValues and ExtractModuleNames hand-roll HCL parsing (a brace-depth scan that counts braces inside strings/heredocs, regexes that don't skip comments) with no direct edge-case test. Hisoka landed the probe rather than reasoning about it: an unbalanced { makes the extractor return nil, which trips require.NotEmpty and fails the test loudly, and these functions are test-only, so the blast radius is a red test on the developer who edits the base, not a mis-installed language in a user's workspace. It fails toward safety, which is why it stays a Note. Robin proposes the structural fix (use hashicorp/hcl/v2 hclsyntax, already a dependency and already used in tfparse.go to scope to a named coder_parameter block), which deletes the hand-rolled scanner and its assumption; a table test over crafted HCL is the lighter alternative. Zoro's counter is fair: a regex cannot balance braces, so the manual scan is the only regex-based option, but that is an argument for the parser, not for the scan.

The collision guard, the two drift invariants, the total-order sort, and the trimmed base all hold across the panel. Nothing here blocks merge.

🤖 This review was automatically generated with Coder Agents.

name = "Web Development"
icon = "/icon/nodejs.svg"
parameters = {
languages = jsonencode(["python", "nodejs"])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-25] The four coder_workspace_preset blocks are a third hand-maintained list of language values that the new CRF-20 guard does not cover (Pariston P3, Melody P3).

The languages set is now declared three times: the selector options, the has_language branches, and the preset languages = jsonencode([...]) arrays (web_dev [python,nodejs], backend_go [go], data_science [python], full_stack [python,nodejs,go]). TestQuickstartLanguageSelectorMatchesInstallScript binds the first two; verified no test reads presets. Today all preset values are a subset of the selector set, so it is latent, the same latency CRF-20 had at P3. If a preset gains a typo (nodjs) or keeps a language the selector later drops, a user who picks that preset gets LANGUAGES set to a value the script's case never matches and nothing installs, with no error. (If the provider validates preset values against the parameter options at plan time, the failure is a build error instead, still untested drift; not verified here.)

Same shape as the existing guard: extract each preset's decoded languages array (a sibling of ExtractParameterOptionValues) and assert it is a subset of the selector option set. If presets are judged low-churn enough to accept the gap, that is a human call and wants a ticket, not a silent drop.

🤖

// are not included. Returns nil if the parameter is absent. The input is
// expected to be rendered output from our own curated base templates, not
// arbitrary user HCL.
func ExtractParameterOptionValues(hcl []byte, paramName string) []string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-19] ExtractParameterOptionValues and ExtractModuleNames hand-roll HCL parsing with no direct edge-case test (Robin P3 structural; Netero, Bisky, Mafu-san, Mafuuu, Zoro, Hisoka Note).

The brace-depth scan counts {/} without awareness of string literals or heredoc bodies, and the regexes do not skip #/// comments, so a { in a parameter description or a commented option in a future base would misparse. Hisoka verified the failure mode empirically: an unbalanced brace makes the function return nil, which trips require.NotEmpty and fails the test loudly, and both functions are test-only, so the blast radius is a red test for the developer who edits the base, not a shipped bug. It fails toward safety, which keeps this a Note.

Two remediations, either fine: Robin's structural fix, use hashicorp/hcl/v2 hclsyntax (already a dependency, already used in provisioner/terraform/tfparse to scope to a named coder_parameter block), which deletes the hand-rolled scanner and its documented "curated input only" assumption; or a table test over crafted HCL (a brace in a description, a heredoc body, a commented option) to lock the boundary. No change is required to ship.

🤖

@github-actions github-actions Bot removed the stale This issue is like stale bread. label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants