feat: add Coder Quickstart base to the template builder - #27247
nickvigilante merged 15 commits into
Conversation
|
/coder-agents-review |
|
Chat: Review posted | View chat Review history
deep-review v0.9.0 | Round 9 | Last posted: Round 9, 31 findings (1 P0, 1 P1, 3 P2, 8 P3, 5 Nit, 13 Note), APPROVE. Review Finding inventoryFinding inventory - PR #27247Findings
Law analysis
Contested and acknowledgedCRF-10 (P3, install-languages.sh.tftpl) - base-vs-example verbatim-copy drift
Round logRound 9Churn guard: PROCEED. Author pushed 3ccaf2c (rebased onto b6d7653). Panel (10: Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Gon, Leorio, Melody, Komugi + wildcard Zoro) + Netero verified CRF-31/32/34 closed and CRF-33 acknowledged (squash-merge moot). NO NEW FINDINGS from any reviewer, first fully clean pass. Mafu-san confirmed CRF-34 was generalized across all four extractor predicates, not patched at one site; Netero confirmed the MCP-server churn in the changed-files list is upstream rebase noise, not this PR's diff. All prior findings resolved or deferred-with-ticket (CRF-6/DOCS-610, CRF-7/DOCS-611, CRF-23/DOCS-614; CRF-3 acknowledged). Event APPROVE (downgraded to COMMENT) + dismiss stale CHANGES_REQUESTED. Review converged: no blocking findings remain. Reviewed against b6d7653..3ccaf2c. Round 8Churn guard: PROCEED. Author pushed dad3379 (PR rebased onto a48aedd). All 4 R7 findings addressed and verified closed by Panel (12) + Netero: CRF-27 (production ExtractAgentResourceName ported off the regex/brace-scan onto shared hclsyntax parseHCLBody, iterating block attributes for count/for_each; deleted agentResourcePattern/agentCountPattern and the regexp import; TestExtractAgentResourceName/CountAfterNestedBlock mutation-tested against the old pattern), CRF-28 (ExtractPresetNames + require.Len count guard binds; mutation-tested), CRF-29 (no CRF- in shipped code), CRF-30 (doc rewritten). No new P0/P1/P2/P3. New (all cosmetic/inert): CRF-31 (Nit, Gon: CRF-27 doc reintroduced edit-history narration, CRF-30 class), CRF-32 (Note, Leorio: parseHCLBody doc omits its production caller), CRF-33 (Nit, Leorio: review-bookkeeping commit subjects; body), CRF-34 (Note, Mafu-san+Melody: inconsistent preset label predicate < 1 vs == 2, inert). CRF-3 golden panel question closed (Bisky: low-value-not-vacuous, no action). Verified CRF-31/CRF-34 in worktree. Do not re-raise CRF-6 (DOCS-610), CRF-7 (DOCS-611), CRF-23 (DOCS-614). Event COMMENT + dismiss stale CHANGES_REQUESTED. PR is essentially ready; only cosmetic polish remains. Reviewed against a48aedd..dad3379. Round 7Churn guard: PROCEED. Author pushed d91e8d5 (PR rebased onto 6a58d50). Both R6 findings addressed: CRF-25 (per-preset require.Subset assertion + negation test in TestQuickstartLanguageSelectorMatchesInstallScript) and CRF-19 (rewrote ExtractModuleNames, ExtractParameterOptionValues, and new ExtractPresetParameterValues to parse via hashicorp/hcl/v2 hclsyntax instead of regex/brace-scan; added TestExtractHCLHelpers covering braces-in-strings, commented options, jsonencode unwrapping). Panel to verify the hclsyntax rewrite's parsing correctness, ExtractPresetParameterValues, and the preset subset guard. Do not re-raise CRF-6 (DOCS-610), CRF-7 (DOCS-611), CRF-23 (DOCS-614). Reviewed against 6a58d50..d91e8d5. Round 6Churn 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 5Churn guard: PROCEED. Author pushed 657abbb, 1cf64f1, 658b74b (PR rebased onto 5f3b875). 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 5f3b875..658b74b. Round 4Churn 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. Round 3Churn 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 2Churn 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. Round 1Netero-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 About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
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.
|
/coder-agents-review |
There was a problem hiding this comment.
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.
|
/coder-agents-review |
There was a problem hiding this comment.
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 toslices.SortFunc+cmp.Compareremains 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
specstable incoderd/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 aspecsrow 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.
|
Re CRF-11 (quickstart absent from the handler Added a 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. |
Documentation CheckThe template builder is enabled by default and its base-selection and module-selection steps are documented in Updates Needed
Note: the base's own Automated review via Coder Agents |
david-fraley
left a comment
There was a problem hiding this comment.
We chatted, follow ups for you -- move it to be next to the Docker template and also remove the IDE selector
|
/coder-agents-review |
There was a problem hiding this comment.
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-clonestill hardcodesregistry.coder.com, and this is structural, not a stray literal.BaseRenderContexthas noRegistryBasefield andrenderBaseis never passed the deploymentRegistryURL, so a base cannot honorCODER_TEMPLATE_BUILDER_REGISTRY_URLthe way wizard-composed modules do. The samegit-clonemodule 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): threadRegistryBaseinto base rendering, dropgit-clonefrom 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/coderpersists), so they reinstall from the network on every start, andstart_blocks_login = trueholds 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.
30fb31b to
658b74b
Compare
|
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 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: |
|
/coder-agents-review |
There was a problem hiding this comment.
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_languagebranches are the same kind of hand-maintained two-list pairing you just made mechanical forincluded_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 sameExtractModuleNames-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
DefaultBaseRenderContextonly, but productionrenderBaseoverlays merged variables, so a base that gated a catalogmoduleblock 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/.cargopersists); 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
groupQuickstartBeforeDockerrationale 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.
658b74b to
bc740d2
Compare
|
R5 findings addressed. All code changes are in CRF-20 (P3) — fixed in CRF-21 (Note) — addressed in CRF-22 (Note) — fixed in 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 Out of scope
Validation on the touched packages: |
|
/coder-agents-review |
There was a problem hiding this comment.
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_presetblocks declarelanguagesa 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. ExtendTestQuickstartLanguageSelectorMatchesInstallScriptto also assert each preset's decodedlanguagesis a subset of the selector options. - CRF-19 (Note):
ExtractParameterOptionValuesandExtractModuleNameshand-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 tripsrequire.NotEmptyand 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 (usehashicorp/hcl/v2hclsyntax, already a dependency and already used intfparse.goto scope to a namedcoder_parameterblock), 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.
jeremyruppel
left a comment
There was a problem hiding this comment.
Non-blocking design suggestion on the base/module collision guard. The current approach keeps two lists in sync (the included_modules manifest field and the modules the base's Terraform actually renders) and adds an invariant test to police the drift. Since the base already declares the modules it bundles by literally rendering them, and Compose already renders the base before validating modules, we could derive the included set from the rendered HCL via the new ExtractModuleNames helper instead of maintaining a parallel manifest field. That removes the concept, the field, and the sync test, and promotes ExtractModuleNames from a test-only helper to a real production caller. Left inline notes on the specific lines. Details below.
Sketch of the derive-from-render approach
// bases.go — replace the manifest field + lookup with a derived, cached set.
var baseIncludedModules = sync.OnceValues(func() (map[string][]string, error) {
bases, err := loadBases()
if err != nil {
return nil, err
}
out := make(map[string][]string, len(bases))
for id := range bases {
mainTF, err := RenderBaseTemplate(id, "main.tf.tmpl", DefaultBaseRenderContext(id))
if err != nil {
return nil, xerrors.Errorf("render base %q: %w", id, err)
}
out[id] = ExtractModuleNames(mainTF)
}
return out, nil
})
func BaseIncludedModules(exampleID string) []string {
all, err := baseIncludedModules()
if err != nil {
return nil
}
return all[exampleID]
}compose.goandtemplatebuilder_handler.gocall sites are unchanged (they already callBaseIncludedModules).- Net: deletes the
included_modulesJSON field, theIncludedModulesstruct field, and the sync test; adds ~15 lines of derive-and-cache. - Two caveats to carry over:
ExtractModuleNamesreturns all module labels (not just catalog IDs), which is harmless here; and the derivation renders underDefaultBaseRenderContext, so amoduleblock gated behind a base variable wouldn't be detected (no base does this today — same limitation the sync test documents, just relocated).
This is a suggestion, not a blocker — happy to discuss if you'd rather keep the explicit manifest field for forward-compat toward preconfigured/base-configured modules.
Generated by Coder Agents on behalf of @jeremyruppel.
jeremyruppel
left a comment
There was a problem hiding this comment.
Follow-up on the extraction helpers and test scope. The selector/script drift this test guards is genuinely worth catching (it's silent and cross-language), but it can be done with a small regex mirroring the existing has_language one, without the HCL extractor family. Bundling piecemeal HCL parsing into this base PR is what creates the test-only exported surface. Suggest keeping regex here and landing the HCL migration as its own focused effort. Inline notes below.
Generated by Coder Agents on behalf of @jeremyruppel.
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).
- CRF-19: replace the hand-rolled HCL parsing in the template-builder base extractors with hclsyntax (matching tfparse.go). A shared parseHCLBody backs ExtractModuleNames and ExtractParameterOptionValues, so braces inside strings or heredocs and commented-out blocks no longer confuse extraction. - CRF-25: add ExtractPresetParameterValues and extend TestQuickstartLanguageSelectorMatchesInstallScript to assert each coder_workspace_preset's languages are a subset of the selector options, closing the third hand-maintained language list. - Add TestExtractHCLHelpers covering the edge cases (braces in a string, a commented-out option block, and jsonencode unwrapping).
- CRF-27: port ExtractAgentResourceName off the regex/brace-scan onto the shared hclsyntax parseHCLBody, so count/for_each is detected wherever it sits in the agent block, not only before the first closing brace. It was the last extractor on the old scanner and the only one on the production composition path. Pre-existing (#26901); ported here since the rewrite established hclsyntax as the in-package parser. Adds a CountAfterNestedBlock regression test. - CRF-28: add ExtractPresetNames and assert len(presetLangs) == len(presetNames), so a preset that set languages to a non-literal would drop out and fail the guard instead of silently evading the subset check. - CRF-29: drop the review-artifact comment prefix from compose_test.go. - CRF-30: rewrite the TestExtractHCLHelpers doc to describe the input under test rather than the removed implementation.
- ExtractAgentResourceName: describe behavior directly instead of narrating the removed regex implementation. - parseHCLBody: note that its one production caller turns a nil body into an error, alongside the test-only extractors that fail their assertions. - ExtractPresetParameterValues: match ExtractPresetNames by requiring exactly two labels on a coder_workspace_preset block, so both halves of the preset count guard use the same predicate. No behavior change; a coder_workspace_preset data block always carries two labels. DOCS-558
… to client Addresses @jeremyruppel's review of the quickstart base. - Derive a base's included catalog modules from its rendered Terraform (cached render + ExtractModuleNames, filtered to catalog IDs) and delete the base.json `included_modules` field, the `IncludedModules` manifest field, and TestBaseIncludedModulesMatchRendered. The base Terraform is now the single source of truth; a new TestBaseIncludedModules covers the derived path. - Move the Quickstart-before-Docker placement off the server (delete groupQuickstartBeforeDocker and its position assertion) and onto the client base-infra select step, mirroring ModuleSelectStep's priority sort via a shared sortByPriority helper. The bases endpoint returns a plain, name-sorted (ID-tiebroken) list. - Revert ExtractAgentResourceName/ExtractModuleNames to the pre-hclsyntax regex extraction and drop the option/preset HCL extractor family, keeping this PR scoped to "add quickstart base". The selector<->install-script drift test uses a small regex instead.
3ccaf2c to
586c53b
Compare
Add vitest unit tests for the client-side base-ordering helper: priority ordering, stability among unlisted items, missing priority ids, input immutability, and the empty-array case.
jeremyruppel
left a comment
There was a problem hiding this comment.
looks great! this is an awesome addition to template builder ❤️
Got Jeremy's passing review, and I implemented David's feedback already
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.
What changed
coderd/templatebuilder/bases/quickstart/:base.json:id: "quickstart",display_name: "Coder Quickstart",os: "linux".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.tftplandREADME.md(with prerequisites markers so the builder can extract the prerequisites section).ModuleSelectStep(sharedsortByPriorityhelper).ExtractModuleNames, cached withsync.OnceValuesand filtered to catalog IDs).validateModulesseeds its seen-set from that derived set, so a wizard-selected module the base already renders is rejected with a clear error, and the modules endpoint omits it so the wizard never offers a colliding module. The base's Terraform is the single source of truth, so there is no separate manifest list that can drift.Testing
go test ./coderd/(template builder handler: bases ordering, thebaseSpectable, the modules-endpoint base filter): pass.go test ./coderd/templatebuilder/(all-bases render/snapshot, the collision guard,TestBaseIncludedModules, the selector/install-script drift test): pass.gofmt,go vet,golangci-lint: clean.tsc,biome, andknip: clean.Review decisions
codercom/enterprise-base:ubuntu). The quickstart base is the opinionated path; the Docker base covers custom images. Revisiting acontainer_imagevariable is tracked in DOCS-611.Review updates (@jeremyruppel)
sortByPriorityhelper, following theModuleSelectStepprecedent. The API just returns a stable, name-sorted list.included_modulesmanifest field and theIncludedModulesstruct field;BaseIncludedModulesnow derives from the rendered base (single source of truth). RemovedTestBaseIncludedModulesMatchRenderedand added a directTestBaseIncludedModules.ExtractAgentResourceName/ExtractModuleNamesto the pre-hclsyntaxregex extraction and removed the option/preset HCL extractor family; the selector↔install-script drift test now uses a small regex. The broader templatebuilder HCL migration and the preset-language subset guard are tracked in DOCS-735.origin/main(no content changes beyond the above).Linear: DOCS-558