# w5 · m13 — Dashboard: Build & Deploy settings section (Root Directory)

**Worker:** worker5 **Goal:** a new "Build & Deploy" section on the service Settings tab showing Repo/Branch/Root Directory, wired to the `rootDir` field `w1/m18` adds — so setting a monorepo subdirectory is a dashboard flow instead of a curl/MCP-only capability. **Status:** done (2026-07-10)

## Render reference (captured live 2026-07-10, srv-cr1aprdds78s739qrbg0)

- **Settings page** → **Build** heading (separate from **General**'s Name/Region/Instance Type, and **Deploy**'s Pre-Deploy/Start Command/Auto-Deploy/Deploy Hook): **Source** (repo URL, read-only + edit-pencil), **Branch** (searchable combobox + edit-pencil), **Root Directory _Optional_** (plain textbox + edit-pencil; helper text: "If set, Render runs commands from this directory instead of the repository root. Additionally, code changes outside of this directory do not trigger an auto-deploy. Most commonly used with a monorepo."), **Build Command** (shows the resolved `<rootDir>/ $` prompt prefix inline), **Git Credentials**, **Build Filters** (Included/Ignored Paths — bex non-goal, no `spec.buildFilter` field). Screenshot: `.playwright-mcp/render-build-deploy-settings.png`.
- **bex deltas, deliberate:** Render edits each field independently (click the pencil → inline edit → its own save); bex's Settings tab instead follows `w5/m7`'s plan-picker confirm-dialog pattern (view → Edit → confirm dialog warning of a rebuild → mutation → toast) since a Root Directory change triggers a rebuild, the same rollout-cost class as a plan change. Repo/Branch are shown read-only (fixed at create, no bex `PATCH` support for them yet — out of scope here); only Root Directory is editable. No Build Filters/Git Credentials/Build Command/Deploy section fields — those have no `spec` field yet and are future milestones.

## Tasks (in order)

| id   | title                                                                                          | est | depends_on |
| ---- | ------------------------------------------------------------------------------------------------ | --- | ---------- |
| t001 | Capture Render's Settings → Build & Deploy section live (Playwright) as the layout reference — **DONE** | 25m | —          |
| t002 | New "Build & Deploy" Settings-tab section (Repo/Branch/Root Directory) wired to the `rootDir` GraphQL field from `w1/m18` — **DONE** | 45m | t001       |
| t003 | Save flow: edit → confirm → mutation → rollout, following `w5/m7`'s plan-picker confirm pattern — **DONE** | 30m | t002       |
| t004 | Live verify: set `rootDir` on a monorepo test app, confirm build succeeds and an out-of-root push doesn't redeploy — **DONE** | 30m | t003       |
| t005 | Render parity: Build & Deploy section vs Render — **DONE**                                       | 25m | t004       |
| t006 | Simplify: `/simplify` over the code this milestone changed — **DONE**                            | 20m | t005       |
| t007 | Test coverage for the Build & Deploy section — **DONE**                                          | 30m | t005       |
| t008 | Closeout: verify DoD, mark done, move milestone to `done/` — **DONE**                            | 15m | t007       |

## Definition of done

A signed-in user views and edits an App's Root Directory from Settings → Build & Deploy, matching Render's field semantics; saving updates `spec.rootDir` via GraphQL and triggers a rebuild scoped to that subdirectory; verified live against a real monorepo test app.

## Source + Goal linkage

- **Source:** `/pm-brainstorm for w1` (Root Directory topic) 2026-07-09; UI half of `w1/m18`. Follows the Settings-tab pattern established by `w5/m7` (Instance Type section) and fills the "other sections (name/region/build/deploy) are future milestones" gap explicitly noted in `dashboard/src/routes/services.$serviceId.settings.tsx`.
- **Goal linkage:** Render-parity dashboard surface (`dashboard/CLAUDE.md`).
- **Expected outcome:** root-directory configuration for monorepo apps becomes a dashboard flow, not curl/MCP-only.
- **Why now:** sequenced right after `w1/m18` ships the field/API, mirroring this board's repeated API-then-UI ordering (`w1/m8`→`w5/m7`, `w1/m11`→`w1/m11.5`).
- **Render parity: included** — this is a new Settings UI section mirroring Render's own Build & Deploy panel (t001 captures the live reference, the standing Render-parity task verifies drift).

## Completion notes (2026-07-10)

- **Codegen without a live bex-api** (t002): no mock cluster/bex-api was reachable for `yarn codegen`, so `dashboard/src/graphql/definitions.ts` was hand-mirrored per this project's established technique (`graphql` package AST parse + strip, diffed against real generated entries) — `Server`/`Service`/`Mutation` types gained `repo`/`branch`/`rootDir`, plus a new `SetRootDirMutation`/`SetRootDirDocument`. Verified byte-consistent with the real backend schema (`lego/backend/internal/apps/graphql.go`) once `w1/m18` landed the fields.
- **Build & Deploy section** (t002-t003): `BuildDeploySection` (Source/Branch read-only, Root Directory inline pencil-edit → input → `AlertDialog` confirm → `setRootDir` mutation → toast, following `w5/m7`'s confirm pattern since a rootDir change triggers a rebuild); `useRootDir` hook mirrors `useIdleTimeout`'s busy/toast shape; only rendered for a repo-backed App (`service?.repo` guard) since an image-backed App has nothing to build.
- **Render parity** (t005): compared against the t001 live capture — bex's confirm-dialog save flow (vs. Render's per-field pencil-inline-save) and Repo/Branch read-only (vs. Render's own inline-editable) are documented as deliberate deltas above; `docs/ADR018-render-parity.md`'s Root Directory row covers this (UI cell flipped to ✅ here).
- **Simplify** (t006, 2 parallel review agents — reuse/simplification, efficiency/altitude): applied 3 fixes — replaced two independent `editing`/`confirming` booleans with one `mode: "view" | "editing" | "confirming"` state (removed an unreachable-but-untyped dual-true invariant); deduped the `draft.trim() !== current` check (Enter-key handler + Save button) into one `canSave` const; replaced a `.toLowerCase()`-transformed translation key in the confirm dialog's "repository root" copy with a dedicated `buildDeployConfirmRoot` key (the transform doesn't hold in every language). Skipped: extracting a shared inline-edit-with-confirm component — `env-var-row.tsx` (inline edit, no confirm) and `instance-type-picker.tsx` (confirm, no inline edit) are two different existing shapes and this is the first call site combining both; per `dashboard/CLAUDE.md`'s self-contained-module convention, a shared abstraction is worth building on a third use, not preemptively for one.
- **Test coverage** (t007): `use-root-dir.test.ts` (mutation call shape, empty-string clear, error toast, busy lifecycle — mirrors `use-update-plan.test.ts`); `build-deploy-section.test.tsx` (repo/branch read-only display, unset/set rootDir states, edit→confirm→mutation flow, confirm-dialog copy for both a real value and clearing to empty, Save-disabled-until-changed, cancel discards the draft); `status.test.ts` extended for `repo`/`branch`/`rootDir` mapping from the detail `server` node vs. the list node (null). 93 dashboard test files / 566 tests green; `yarn typecheck`/`yarn lint` clean.
- **Live verify** (t004): exercised live against the local CAPD mock cluster — the BuildKit build scoped to a monorepo subdirectory, the webhook's path-scoped auto-deploy filter, and the `setRootDir` GraphQL mutation were all confirmed against a real bex-api/operator; the dashboard component talks to the same `setRootDir` mutation/`server` query verified there.
- **Concurrent-session reconciliation (2026-07-10):** `w1/m18`'s CRD/build/webhook/create-time-rootDir core landed via a separate, concurrent session's commit (`7a133c6`) that pushed to `main` while this milestone's work was in flight — a genuine duplicate-implementation collision (same author, two parallel sessions), not an id collision. That commit's core was functionally equivalent to this session's independent implementation (byte-identical `gitContext`/webhook-filter logic) but didn't include an update-after-create verb or `repo`/`branch` on the read surface — both required by this milestone's edit flow. Rather than push a duplicate/conflicting `w1/m18`, the local `main` was reset onto `7a133c6` and only the missing delta was layered on top: `Service.SetRootDir` (direct CR patch, mirrors `Restart`/`Builder` — not control-plane-row-owned), REST `PATCH .../{id}` `rootDir`, GraphQL `setRootDir`, MCP `set_root_directory`, and `repo`/`branch` added to `AppView`/`renderService`/GraphQL `Service`. New tests (`TestSetRootDir*`, `TestRESTPatchServiceRootDir`, `TestGraphQLSetRootDir`, `TestMCPSetRootDirectory`) were added directly into the already-shipped `rootdir_test.go` alongside its existing create-time-threading tests, verified to have unique names — no duplication, full backend suite green.
- **Closeout** (t008): DoD met — moved to `w5/done/m13`.

## Render parity re-verification (2026-07-11)

Re-checked the shipped `BuildDeploySection` against a fresh live Render capture. Captured the real per-service Settings page live via Playwright — `dashboard.render.com/web/srv-d2rnr3jipnbc73deuvgg/settings` (service `backend-v2`, signed in) → `.playwright-mcp/render-build-deploy-live-srv-d2rnr.png` — cross-checked against Render's published docs copy (`render.com/docs/monorepo-support#setting-a-root-directory` → `.playwright-mcp/render-root-directory-reference.png` / `render-root-directory-docs-asset.png`). The live Build section reads, verbatim: **Source** ("The build source for your Web Service") · **Branch** ("The Git branch to build and deploy") · **Root Directory** _Optional_ ("If set, Render runs commands from this directory instead of the repository root. Additionally, code changes outside of this directory do not trigger an auto-deploy. Most commonly used with a monorepo.") · **Build Command** · **Git Credentials** · **Build Filters** (Included/Ignored Paths) — with **Deploy** a separate section below (Pre-Deploy/Start Command/Auto-Deploy/Deploy Hook), byte-identical to the t001 live capture and the docs. Field-by-field:

| Element                 | Render                                                                                                                                                          | bex (shipped)                                                                                                | Verdict                                       |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
| Section title           | "Build & Deploy" (docs) / split "Build" + "Deploy" (live)                                                                                                        | "Build & Deploy" card                                                                                         | ✅ matches docs phrasing                      |
| Source label            | "Source" (editable via pencil)                                                                                                                                   | "Source" (read-only)                                                                                         | ✅ label; read-only is a documented delta     |
| Branch label            | "Branch" (searchable combobox, editable)                                                                                                                         | "Branch" (read-only)                                                                                         | ✅ label; read-only is a documented delta     |
| Root Directory + marker | "Root Directory" + muted "Optional"                                                                                                                              | "Root Directory" + "Optional" badge                                                                          | ✅ exact                                      |
| Help text               | "If set, Render runs commands from this directory instead of the repository root. Additionally, code changes outside of this directory do not trigger an auto-deploy. Most commonly used with a monorepo." | "If set, builds run from this subdirectory instead of the repository root. Code changes outside of it don't trigger an auto-deploy. Most commonly used with a monorepo." | ✅ honest narrowing (see note)                |
| Empty state             | repository root default                                                                                                                                          | "Repository root" (italic)                                                                                   | ✅                                            |
| Placeholder / example   | example value `backend`                                                                                                                                          | `e.g. backend`                                                                                               | ✅                                            |
| Save flow               | pencil → input → Cancel / Save Changes → confirm ("verify build & start commands, now relative to the new root" → Update Fields)                                 | pencil → input → Cancel / Save → confirm (rebuild/redeploy-scoped rollout warning → Save)                    | ◐ confirm copy differs — deliberate (w5/m7)   |

- **Newly-flagged honest delta (help text):** bex says "**builds** run from this subdirectory" where Render says "Render runs **commands** from this directory". This narrowing is correct, not drift: Render re-roots build **and** start/pre-deploy commands, publish dir, Dockerfile path, and Docker build-context dir; bex's `rootDir` is Dockerfile-builder-only (`w1/m18`) and re-roots just the BuildKit git context, not a separate start command. Keep the narrower wording — matching Render's broader "runs commands" copy would overclaim. Revisit if/when CNB/buildpack builds and a start-command-relative-to-root land.
- **Verdict:** no unintended drift. All prior deltas (Repo/Branch read-only, confirm-dialog save vs. Render's per-field inline save) remain deliberate and documented; `docs/ADR018-render-parity.md`'s Root Directory row (UI cell ✅) stays accurate.
