IIS Worktree Branch Previews¶
Run a feature branch's portal + admin SPA + API + backend + login in local IIS side-by-side with the canonical local environment and with other branches, each under its own branch-specific hostnames, so engineers, product, and support can review a branch — including the full sign-in round-trip — without anyone having to swap their local IIS mapping.
Two scripts implement this:
scripts/dev/create-branch-preview.ps1— provision a previewscripts/dev/remove-branch-preview.ps1— tear it down
Quick start¶
Run from an elevated PowerShell (IIS and the hosts file require admin):
# Stand up a preview for a branch (worktree is created automatically).
# -Org is the organisation the portal serves (default: gercek).
scripts\dev\create-branch-preview.ps1 -Ticket rak349 -Branch rakletai/rak-349-apply-form-validation-error-leak -Org gercek
# ... review at the URLs printed below ...
# Tear it down (keeps the worktree by default)
scripts\dev\remove-branch-preview.ps1 -Ticket rak349
# Tear down AND remove the worktree it created
scripts\dev\remove-branch-preview.ps1 -Ticket rak349 -RemoveWorktree
create-branch-preview.ps1 prints the preview URLs for ticket rak349:
| Role | URL | Served from |
|---|---|---|
| Portal | https://rak349-gercek.raklet.org |
Application (serves the -Org org; label is the org permalink) |
| Admin | https://rak349-admin.raklet.org |
Raklet.Admin |
| API | https://rak349-api.raklet.org |
Raklet.Api |
| Backend (admin assets) | https://rak349-backend.raklet.org |
Raklet.Backend |
| Login | https://rak349-login.raklet.org |
Raklet.Login |
-Ticket is a short DNS-safe token (lowercase letters, digits, dashes). It names
both the hostnames and the preview-<ticket>-* IIS sites/app pools.
What the create script does¶
- Reuses the branch's existing git worktree if it already has one; otherwise
creates
.claude/worktrees/<ticket>for the branch. - Junctions
packages/to the main checkout (so transitive NuGet HintPaths resolve —packages/is git-ignored and does not copy into new worktrees). - Seeds the git-ignored
*.local.configfiles (DB connection string + storage credentials) from the main checkout. Every preview shares the one local database, which is intended. - Rewrites the
admin/api/backend/loginhost references inside each worktree'sweb.config(and anyappSettings.local.config) to the branch hosts. This covers the*-PathappSettings used for server-side redirects and server-rendered links and the credentialed CORS allow-lists (v2:origins/internal:origins). Rewritinglogin(incl. the login site's ownRaklet-Admin-Path) is what keeps the whole sign-in round-trip on the preview. The portal hosts (www/v3),crm,CookieDomain, andname@raklet.orgemails are left canonical —CookieDomainmust stayraklet.orgso the auth cookie is shared across the preview hosts. - Sets
Preview-Organisation-Permalinkin the worktree'sApplication/web.configto-Orgso the portal serves that organisation (see "How the portal preview works" below). - Builds
Application,Raklet.Api,Raklet.Admin,Raklet.Backendand rebuilds the admin assets with Grunt (build-local). Pass-SkipBuildto provision against an already-built worktree. - Creates one app pool + one site per role, with
http+httpsbindings on the branch hostnames, and assigns each site its pool. - Adds
127.0.0.1hosts-file entries for the five hostnames. - Warms each site over HTTPS and prints the URLs.
backendis warmed first because it is the only app that applies EF migrations (every other app setsDatabase.SetInitializer<RakletDb>(null)); if it can't start/migrate the script aborts rather than warm the schema-dependent hosts onto an un-migrated DB. See Database migrations and warmup order.
The script is idempotent: re-running it repoints paths, re-applies the host
rewrite (the match is anchored so it never double-prefixes), tops up hosts
entries, and leaves existing IIS objects in place. It only ever touches
preview-<ticket>-* objects — never the canonical v3/admin/api/backend/
login sites.
Why it works (design rationale)¶
The local app is six IIS sites sharing one *.raklet.org wildcard certificate
and one local database. A few properties make branch hostnames cheap and correct:
- Single-label wildcard cert.
CN=*.raklet.orgcovers exactly one label, so a one-label host likerak349-admin.raklet.orgis valid, while a two-label host likeadmin.rak349.raklet.orgis not. That is why the branch token is a prefix on the service label (<branch>-admin), not a parent domain. - Non-SNI shared cert binding. The existing HTTPS bindings share the
0.0.0.0:443certificate, so a new*:443:<host>binding inherits the wildcard cert automatically — nonetsh, no per-host certificate work. - Local wildcard DNS. The dev VM resolves
*.raklet.orgto127.0.0.1(via Acrylic DNS Proxy), so branch hostnames resolve without DNS-provider changes. The hosts-file entries the script adds are a portable fallback for machines without the wildcard resolver. - Host-derived roots. The admin SPA computes
ApiRoot/AdminRoot/BackendRoot/LoginRootfrom the hostname that served the page (Raklet.Backend/Content/scripts/core/app.module.js, local branch). The same admin bundle served fromrak349-admin.raklet.orgtherefore callsrak349-api.raklet.organd signs in viarak349-login.raklet.org; the canonicaladmin.raklet.orgstill usesapi.raklet.org/login.raklet.org. Build once, works for every branch host.PathService.getLoginUrlreads the derivedLoginRoot. - Server-side host rewrite. The MVC admin shell redirects and the
server-rendered links read host values from
web.configappSettings, which are rewritten per worktree (step 4) so they stay on the preview host instead of bouncing to the canonical sites.
Login round-trip. The admin SPA sends an unauthenticated user to the
host-derived rak349-login.raklet.org. That login site is the Raklet.Login
project with its own rewritten web.config, so after authentication its
PathService.GetAdminPathNG() (reading the rewritten Raklet-Admin-Path)
redirects back to rak349-admin.raklet.org — the visitor never leaves the
preview. The auth cookie is scoped to raklet.org (CookieDomain, left
canonical), so the session is shared across every *.raklet.org preview host and
the canonical local env. (Social/OAuth sign-in still uses provider redirect URIs
registered for login.raklet.org, so use email/password on a branch login host.)
How the portal preview works¶
The member-facing portal (Application) is tenant-keyed by hostname:
Application/Infrastructure/SubdomainRoute.cs normally reads the subdomain as an
organisation permalink (demo.raklet.org → org demo), so a branch host like
rak349-gercek.raklet.org would resolve to a non-existent org rak349-gercek and 404.
The route therefore honours a Preview-Organisation-Permalink appSetting: when
set, it serves that organisation regardless of the host and skips the
subdomain/custom-domain resolution. The key ships empty in the
canonical/staging/prod Application/web.config (a no-op there); the create
script sets it to -Org in the worktree copy. The portal host's label is the
org permalink (<ticket>-<org>) purely so it's self-documenting and consistent
with the other services — routing is driven entirely by the appSetting, not by
parsing the host (org permalinks can contain dashes, so the host isn't parsed).
Portal self-links built via PathService.GetV3Path already resolve to the current
request host, so a preview visitor stays on rak349-gercek.raklet.org while browsing.
Two branches can preview the same org in parallel (e.g. both -Org gercek)
at their own hostnames, sharing the org's data in the one local database.
Scope and limitations¶
- Shared local database. All previews (and the canonical local env) use the
same database. Do not run two branches in parallel when they have diverging
EF migrations — one branch's
Update-Databasewill break the other. The portal org's data is shared too, so concurrent mutating tests on the same-Orgcan interfere; use different orgs per branch when that matters. - Migrations run via
backendonly. A preview's schema is current only after itsbackendsite has started and migrated. If a write 500s with EF's "An error occurred while executing the command definition" (inner:Invalid column name), the DB is behind the model — warm/fixbackendfirst. See Database migrations and warmup order. - Login is per-branch, session is shared. Each preview gets its own
<ticket>-login.raklet.orgso the sign-in round-trip stays on the preview, but the auth cookie is scoped toraklet.org, so one sign-in is shared across all previews and the canonical local env. Social/OAuth providers only allowlogin.raklet.orgredirect URIs, so use email/password against a branch login host. - Cleanup. Remove previews you are done with so IIS sites, app pools, and
hosts entries do not accumulate. Use
-RemoveWorktreeonce the branch work is finished (the script refuses to remove a worktree with uncommitted changes and tells you the--forcecommand if you really mean it).