Repository navigation
feat: full branding implementation on deployment #148
Description
Revised issue: Rebrand the quick-start per customer (template packaging for src, a script for full-repo clones)
Problem
The repo must run out of the box as GoodToCode branding (the GoodToCode organization and the Microsoft Agent Framework Quick-start product). Customers then need it branded as their organization and product.
Constraint 1: agent-framework-quick-start must build and run immediately after git clone, following the README steps. The source contains real GoodToCode values only. It contains no placeholders, no template conditionals and no unresolved tokens. All tokenization is the job of the template repo's packaging scripts, which transform a staged copy of src at packaging time.
Constraint 2: Build-DotnetTemplateRelease.ps1 and Build-VsixTemplateRelease.ps1 work today, and the VSIX deploys correctly for both web and API. Their current output is the baseline. All new work is additive, switchable and regression-tested against that baseline. Nothing in the existing flow is rewritten.
There are two delivery scenarios, each with a different scope and mechanism.
| Scenario | How the customer gets it | What they receive | Rebrand mechanism | Where the logic lives |
|---|---|---|---|---|
| 1. Template | dotnet new agentweb or the VSIX |
Only agent-framework-quick-start/src, plus the VSIX root.vstemplate, which creates the solution |
Template-native substitution (symbols, VS $tokens$) applied to tokens injected into the staged copy |
agent-framework-template packaging scripts |
| 2. Clone | git clone of the quick-start repo |
Everything: src, solution files, .github, .azure, .azuredevops, docs, data, README.md, LICENSE, dev scripts |
Repo-wide PowerShell script, Update-Branding.ps1 |
agent-framework-quick-start repo root (not deployed) |
Packaging scope is not changing. Both templates ship src only. README.md, docs/, .github/, .azure/, .azuredevops/, data/, dev scripts, LICENSE and the root .slnx files are not part of either template. The playbooks stay in src as a core part of the product.
How tokenization works today (findings)
dotnet new (Build-DotnetTemplateRelease.ps1)
Pipeline: Update-DotnetTemplateWebContent.ps1 (sync), Install-DotnetTemplateWeb.ps1, Test-DotnetTemplateWebCreate.ps1.
- The sync step only copies
quick-start/srcinto.template.dotnet/web-fullstackand removesbin,obj,TestResults,.vs,.git,swagger,*.user,*.suo,*.cache,*.nupkg,*.zip,swagger.json. It does no tokenization. - The only substitution is
sourceName: "Goodtocode.AgentFramework"in.template.config/template.json.dotnet newapplies it as a literal replace across file contents and file names. This renames namespaces, assembly names and the Razor scoped-CSS reference consistently. There are no symbols. - Everything else GoodToCode passes through into the customer's solution untouched (display names, SEO text, emails, tenant host name,
UserSecretsIdvalues).
VSIX (Build-VsixTemplateRelease.ps1)
Pipeline: bump version, Update-Vstemplate-*.ps1 (check, fix, check), Compress-Vstemplate-*.ps1, build the VSIX solutions. It works for both web and API.
Compress-Vstemplate-*.ps1copies thevstemplatefolder andsrcinto.template.vsix/temp, then usesUpdate-ContentsByTagto set<RootNamespace>,<AssemblyName>and<DocumentationFile>to$safeprojectname$(and$safeprojectname$.xml) in.csprojfiles.Presentation.Web.csprojis intentionally excluded from the namespace and assembly swap. Its assembly name drives the scoped-CSS bundle reference inApp.razor(Goodtocode.AgentFramework.Presentation.Web.styles.css), so leaving it literal is what keeps the web template working.- Every
ProjectItemin the.vstemplatefiles hasReplaceParameters="true", so Visual Studio expands any$token$found in those files. - Code namespaces (
namespaceandusingstatements) stayGoodtocode.AgentFramework.*in the VSIX output. This is the proven, working behavior.
Consequence
Neither template handles display text, org identifiers, the tenant host name or UserSecretsId values. dotnet new renames the namespace prefix. The VSIX keeps code namespaces as GoodToCode by design. Renaming VSIX code namespaces is not a goal of this issue, because it would put the working Web scoped-CSS wiring at risk. It can be a separate, later issue.
Clone-and-run guardrails (must not change)
The README clone flow must keep working exactly as written, and rebranding must not break it. These are contracts, not branding, and are never rebranded by either track:
- The Entra scripts in
.azure/scripts/entra/*and their parameter names. The app registration names are caller-supplied (myproduct-web-dev-001). - The API scopes
access_as_user,assets.read,assets.edit,assets.delete. - The user-secrets keys:
EntraExternalId:*,BackendApi:ClientId,AgentProvider:Kind,AzureOpenAI:*,ApplicationInsights:ConnectionString. - The EF context and database names (
AgentFrameworkContext,Initial Catalog=AgentFramework) and the commands in the README that use them. - The launch URL and port (
https://localhost:6195) that the Entra scripts register. - Any
Brandingdefault added tosrcmust live in committed appsettings with GoodToCode values. Branding must never require a new secret or a new setup step.
Per-project state that must not leak between clones
- Each of
Presentation.Api,Presentation.WebandTests.Integrationhas a fixed<UserSecretsId>. A template-created solution or a rebranded clone that keeps the same IDs shares one secrets store with the original clone on the same machine. Secrets written bydotnet user-secrets setor byNew-EntraAppRegistrations.ps1would silently apply to the wrong product. appsettings.Development.jsonandappsettings.Production.jsoncontainhttps://goodtocodesecuredev.ciamlogin.com/. That is a GoodToCode tenant host. For customers it should become the README's own placeholder form,https://your-tenant-name.ciamlogin.com/, or the customer's value.
Current state of branding in src
| Category | Examples found in src |
Handled by dotnet new? |
Handled by VSIX? |
|---|---|---|---|
Namespace, assembly, root namespace (Goodtocode.AgentFramework.*) |
.cs, .razor, .csproj, Generate-NswagClientCode.*, EF migrations |
Yes (sourceName) |
Project metadata only (by design; code namespaces stay literal) |
Product display name Quick-start for Microsoft Agent Framework |
Presentation.Web/Shell/App.razor (description, og, twitter), MainLayout.razor, HomePage.razor <PageTitle>, Swagger title in Presentation.Api/ConfigureServices.cs |
No | No |
Org display name GoodToCode or Goodtocode |
og:site_name, footer © @DateTime.Now.Year Goodtocode, logo alt, wwwroot/ai-manifest.json, wwwroot/llms.txt, Swagger Contact and License, GlobalPreamble in the 4 appsettings files |
No | No |
| Org and repo identifiers | developers@goodtocode.com, jdoe@goodtocode.com (test data), the GitHub license URL in Swagger |
No | No |
| Tenant host | goodtocodesecuredev.ciamlogin.com in appsettings |
No | No |
| Per-project secrets identity | 3 <UserSecretsId> GUIDs in the csproj files |
No | No |
| Assets | wwwroot/img/logo.svg (aria-label="GoodToCode logo") |
No | No |
Over-matching risk
Goodtocode.* is also the NuGet package namespace that the app depends on:
Goodtocode.MediatorGoodtocode.DomainGoodtocode.ValidationGoodtocode.Agents.PlaybookGoodtocode.Agents.Playbook.PromptingGoodtocode.Agents.GovernanceGoodtocode.SecuredHttpClientGoodtocode.Assertion
A naive replace of Goodtocode, or even a case-sensitive GoodToCode in a template replaces, can break using statements and PackageReference entries. Any wider rebranding, in either scenario, must allow-list exactly these dependencies. Because of this, the packaging step injects unique tokens for brand text instead of relying on literal replaces for anything other than the existing namespace rule.
Recommendation: two tracks, one shared map and engine, additive changes only
Shared: branding.map.json and the replace engine (quick-start repo root)
A single data file defines the brand conventions. It is data only and has no effect on build or run. It defines:
- The ordered replacement rules, longest token first. For example:
Quick-start for Microsoft Agent Framework-> product nameGoodToCode Agent Framework-> company name plus product short nameGoodtocode.AgentFramework-> namespace prefix (clone script only; templates already handle it)GoodToCode/Goodtocode/goodtocode(display, slug, domain, email forms) -> companygoodtocode/agent-framework-quick-start-> repo owner/name (URLs, badges, clone and license links)goodtocodesecuredev.ciamlogin.com-> tenant host
- The dependency allow-list (
Goodtocode.Mediator,Goodtocode.Domain,Goodtocode.Agents.*, and so on). - The contract allow-list from the guardrails above: scope names, user-secrets keys, EF context and database names, ports.
- The excluded paths and extensions (
bin,obj,TestResults,.git,.vs,*.nupkg, binaries). - The file-kind handling (JSON, XML, YAML, text, markup) and the encoding rules (preserve BOM and line endings).
Update-Branding.ps1 is the single engine. It supports two output targets:
-Target Values: writes the customer's values (Track 2, clone).-Target DotnetNew/-Target Vsix: writes tokens (Track 1), run by the template packaging scripts against the staged copy only.
Trade-off to decide in the implementation: the template repo then depends on the quick-start script. It already depends on the sibling src path, so this is the same coupling. The alternative is a duplicated engine in the template repo, which would let the two tracks drift apart.
Track 1: Template-based branding of src (template repo; no packaging structure change)
1a. Quick-start src: keep it literal, with optional light cleanup
- No tokens, placeholders or
#ifblocks insrc. - Optional, low-risk cleanup that does not affect clone-and-run:
- Normalize casing and wording so each brand role uses one spelling (
Goodtocodefor namespace,GoodToCodefor display, one fixed product name), so the map matches reliably. - Centralize repeated display strings, for example in a
Brandingoptions section with GoodToCode defaults in committed appsettings. This is optional and is not required by either track. - Keep
Goodtocode.<package>references strictly to the NuGet allow-list.
- Normalize casing and wording so each brand role uses one spelling (
- Do not hand-edit generated artifacts (
swagger/**,BackendApiClient.g.cs, EF designer files).
1b. Template repo: add one tokenization step after the existing copy
Principle: do not change what already works. Each release script keeps its current steps and order. The new step is inserted after the existing copy and before compress or install, is controlled by a switch (for example -ApplyBranding, default off until validated, then on), and is covered by a before/after regression check.
dotnet new (Build-DotnetTemplateRelease.ps1, Update-DotnetTemplateWebContent.ps1, template.json):
- After the existing copy, run the engine with
-Target DotnetNewon.template.dotnet/web-fullstack. This turns display text, emails, repo URLs and the tenant host into unique tokens, outside the allow-lists. It does not touch theGoodtocode.AgentFrameworknamespace rule. template.jsonkeepssourceName: "Goodtocode.AgentFramework"and adds symbols (ProductName,CompanyName,ContactEmail,RepositoryUrl,Description,TenantHost) whosereplacestarget the new tokens. Use the XML, JSON and HTML encodeformswhere a value lands in markup or JSON (verify availability).- Add generated GUID symbols, one per
UserSecretsId, so each created solution gets its own IDs. Test-DotnetTemplateWebCreate.ps1additionally asserts that the generated output builds and contains no GoodToCode display text or tenant host outside the allow-lists.- Decide whether
web-fullstackstays committed in the template repo. If it does, the tokenized output appears in diffs, and the sync step must be idempotent.
VSIX (Build-VsixTemplateRelease.ps1, Compress-Vstemplate-*.ps1):
- After the existing copy and the existing
Update-ContentsByTagedits inCompress-Vstemplate-*.ps1, run the engine with-Target Vsixon.template.vsix/temp. It injects only non-namespace tokens:$guid1$..$guid3$for theUserSecretsIdvalues, and tokens for display text, emails, repo URLs and the tenant host, with$registeredorganization$or$customparameters$defaults where practical. - Leave unchanged: the
$safeprojectname$edits, thePresentation.Web.csprojexclusion, code namespaces,root.vstemplateand theUpdate-Vstemplate-*.ps1drift checks. - Confirm each file that now carries a token has
ReplaceParameters="true"(the existingUpdate-Vstemplate-*.ps1fix mode already manages this list, so no new mechanism is needed). - Where a replacement touches a file, preserve its encoding and BOM (the existing
Set-Content-based helpers can change them, so the engine must handle this itself). - Add the leftover check (unreplaced GoodToCode display text, unresolved tokens, or the tenant host) between compress and build, and fail the release on a hit.
- Regression check: with the switch off the VSIX zip is identical to today's. With it on, the web and API VSIX builds still succeed and create a building solution.
Track 2: Repo-wide Update-Branding.ps1 (quick-start repo root, not deployed by either template)
For the clone scenario, where every file is present. It uses branding.map.json and covers what the templates do not ship.
Scope:
src/**: the same replacements as Track 1, with-Target Values, plus the namespace prefix (Goodtocode.AgentFramework-> the customer namespace) across.cs,.razor,.csproj,Generate-NswagClientCode.*and EF migrations. It must update the Razor scoped-CSS reference inApp.razortogether with the assembly name.- Solution files: rename
Goodtocode.AgentFramework.Web.slnx,Goodtocode.AgentFramework.Api.slnxand the.slnLaunch.userfile. .github/**: workflows (gtc-agent-standalone-*.ymlfile names and theSRC_SLNvalue),dependabot.ymlreviewers,copilot-instructions.md,instructions/*.md, repo scripts..azure/**(Bicep names and parameters) and.azuredevops/**(pipeline names, variables such asapprover.email@goodtocode.com). Excluding the Entra scripts' scope names and parameter contracts..vscode/settings.json(dotnet.defaultSolution).docs/**, including prose that says "GoodToCode" or "Quick-start for Microsoft Agent Framework" and theai-seo-guide.mdJSON-LD.data/**and root scripts (Reset-EfDatabase.ps1,Reset-EfNswag.ps1,Update-NSwag.ps1).README.md: badges, clone URLs, links to GitHub, social links, and the solution file name used in its build commands. The setup commands, user-secrets keys, EF context and database name, and Entra script paths and parameters stay as they are.LICENSE(copyright holder line only, with an explicit confirmation flag).- Assets: swap
logo.svgand optionally the favicon from a supplied path. - Regenerates the three
UserSecretsIdvalues (opt out with-KeepUserSecretsIds).
Behavior:
- Takes parameters or a
branding.jsonwith:NewCompany,NewProduct,Namespace,Domain,ContactEmail,RepoOwner,RepoName,TenantHost,LogoPath,Copyright. The old values come frombranding.map.json. - Applies the replacement map in priority order and never touches the dependency allow-list, the contract allow-list or the excluded paths.
- Understands file kind: JSON through its parser, XML for
.csprojand.sqlproj, YAML as text with a key-aware pass forname:andSRC_SLN, Bicep as text,.svg,.razor/.html, markdown. It preserves encoding, BOM and line endings. - Renames files, folders and solution files, then updates every reference to them (workflows, pipelines,
.vscode, scripts, docs, README commands). -WhatIf(dry run) is the default. It prints a per-file change report and a leftover scan that fails if any unmappedgoodtocode/Microsoft Agent Framework quick-starttext remains, apart from reviewed exceptions (the allow-lists andLICENSEtext).-Applyperforms the changes. It refuses to run on a dirty git tree unless-Forceis given.- After
-Applyit prints the follow-up steps from the README flow: create the Entra app registrations and set the user-secrets for the newUserSecretsIdvalues, and updateWebRedirectUriandWebLogoutUriif the launch port changed. It does not run the Entra scripts or touch secrets itself. - Is idempotent and writes a change manifest. Leaves all changes local and uncommitted.
- Has a Pester test: run it over a temp copy of the repo and assert that
dotnet buildsucceeds, the README commands still resolve (solution names, project paths, EF context), the allow-lists are untouched and the leftover scan is clean.
Options compared
| Option | Pros | Cons | Verdict |
|---|---|---|---|
A. One additive packaging-time tokenization step on the staged copy, plus dotnet new symbols and VSIX parameters, for src |
Keeps the clone runnable and the source literal. Existing working pipelines are unchanged. | Covers src only. VSIX parameters are weak. |
Track 1 |
| B. Repo-wide PowerShell script | Handles every file in a clone, including solution names, CI, IaC, docs and code namespaces | Over-matching risk without the shared map and allow-lists | Track 2 |
C. Runtime Branding config with GoodToCode defaults |
No rebuild. Clone still runs. Good for deployment-time branding. | Does not cover namespaces, file names or non-src files |
Optional |
| D. Tokens or template syntax in the quick-start source | Simple template step | Breaks clone-and-run | Rejected |
| E. Ship the full repo in the templates with profiles | One mechanism for everything | Changes packaging, risks the repo's intent (playbooks are core), and the VSIX cannot place non-project files cleanly | Rejected |
| F. Rework the VSIX namespace handling | Fully renamed code in VSIX output | Puts the working Presentation.Web scoped-CSS wiring at risk | Out of scope |
| G. Separate engines in each repo | No cross-repo dependency | The tracks drift apart | Rejected unless the shared engine proves impractical |
Recommendation: Track 1 (one additive tokenization step) for template users and Track 2 (Update-Branding.ps1) for cloners, both driven by branding.map.json and one engine.
Out of scope
- No change to which folders are packaged. Both templates keep shipping
srconly. - No change to the existing steps, order or output of
Build-DotnetTemplateRelease.ps1andBuild-VsixTemplateRelease.ps1beyond the one added step. - No change to VSIX namespace handling or to the
Presentation.Web.csprojexclusion. - No restructuring of the playbooks. They remain a core part of
src. - No new template content for README, docs, CI or IaC.
- No tokens or template syntax in the quick-start source.
- No change to the Entra scripts, scope names, user-secrets keys, EF context or database names.
Acceptance criteria
- A fresh clone of the quick-start repo builds and runs by following the README with no extra steps, and the UI, Swagger and SEO output are identical to today's. No placeholders or unresolved tokens exist in
src. - With the new switch off, both release pipelines produce the same output as today. With it on, the
dotnet newtemplate and the web and API VSIX still build and create building solutions. branding.map.jsonis the only definition of the replacement rules, the dependency allow-list and the contract allow-list, consumed byUpdate-Branding.ps1and by both template packaging scripts.dotnet new agentweb -n Contoso.Chat --ProductName "Contoso Assist" --CompanyName Contosobuilds, and its UI, Swagger andllms.txtshow Contoso. The createdPresentation.Api,Presentation.WebandTests.Integrationprojects each have their own newUserSecretsId, and the committed appsettings contain no GoodToCode tenant host.- A VSIX-created solution builds, has new
UserSecretsIdvalues, shows the default or supplied display text where defined, and has no unresolved tokens. - The packaging scripts fail if the staged output still contains GoodToCode display text, the tenant host or an unresolved token outside the allow-lists.
Update-Branding.ps1 -WhatIfon a fresh clone reports changes with no unreviewed leftovers.-Applyproduces a repo that builds, passesdotnet test, has renamed namespaces, solution, workflow and IaC files with all references updated, and whose README commands still work.- NuGet dependencies (
Goodtocode.Mediator,Goodtocode.Agents.*, and so on), Entra scope names and user-secrets keys are unchanged in all paths. - Docs are updated: quick-start README (rebranding section) and the template repo READMEs.
Suggested work breakdown
- quick-start: add
branding.map.json(rules, both allow-lists, file-kind rules). Optionally normalize casing and centralize display strings with GoodToCode defaults, verifying that the README clone flow is unchanged. - quick-start: write
Update-Branding.ps1(engine with-Target Values|DotnetNew|Vsix),branding.sample.jsonand Pester tests. - template (
dotnet new): add the engine call toUpdate-DotnetTemplateWebContent.ps1behind a switch, then the symbols, the generated GUID symbols and the leftover assertion inTest-DotnetTemplateWebCreate.ps1. - template (VSIX): add the engine call and the leftover check to
Compress-Vstemplate-*.ps1andBuild-VsixTemplateRelease.ps1behind a switch, with$guid#$and display tokens only. Add the before/after regression check. - quick-start and template: document both scenarios in the READMEs. Add the end-to-end checks to CI.
Two-variant VSIX build (web and api)
Build-VsixTemplateRelease.ps1 builds two VSIX packages from the two solution roots in the clone (Goodtocode.AgentFramework.Web.slnx and Goodtocode.AgentFramework.Api.slnx). Each Compress-Vstemplate-*.ps1 rebuilds .template.vsix/temp, selects the matching root.*.vstemplate (copy and delete), and zips the result into vsix.web or vsix.api. dotnet new has a single full-stack template.
Requirements:
- The branding engine call and the leftover check run inside both
Compress-Vstemplate-Web.ps1andCompress-Vstemplate-Api.ps1, after the existing steps, on that variant's temp copy only. - The leftover check and the
$guid#$tokens are variant-aware. The API variant has noPresentation.Web, so Web-only files, tokens andUserSecretsIdvalues are neither expected nor flagged there. - The existing
root.*.vstemplatecopy/delete logic is not modified. - Regression and build checks cover both VSIX packages (switch off: identical to today; switch on: both still build and create building solutions).
Update-Branding.ps1renames both.slnxfiles and theWeb.slnLaunch.userfile, and updates references to either in workflows, pipelines,.vscodeand the README build commands.
Activity