Skip to content

feat: full branding implementation on deployment #148

Closed

Description

@goodtocode

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/src into .template.dotnet/web-fullstack and removes bin, 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 new applies 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, UserSecretsId values).

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-*.ps1 copies the vstemplate folder and src into .template.vsix/temp, then uses Update-ContentsByTag to set <RootNamespace>, <AssemblyName> and <DocumentationFile> to $safeprojectname$ (and $safeprojectname$.xml) in .csproj files.
  • Presentation.Web.csproj is intentionally excluded from the namespace and assembly swap. Its assembly name drives the scoped-CSS bundle reference in App.razor (Goodtocode.AgentFramework.Presentation.Web.styles.css), so leaving it literal is what keeps the web template working.
  • Every ProjectItem in the .vstemplate files has ReplaceParameters="true", so Visual Studio expands any $token$ found in those files.
  • Code namespaces (namespace and using statements) stay Goodtocode.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 Branding default added to src must 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.Web and Tests.Integration has 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 by dotnet user-secrets set or by New-EntraAppRegistrations.ps1 would silently apply to the wrong product.
  • appsettings.Development.json and appsettings.Production.json contain https://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.Mediator
  • Goodtocode.Domain
  • Goodtocode.Validation
  • Goodtocode.Agents.Playbook
  • Goodtocode.Agents.Playbook.Prompting
  • Goodtocode.Agents.Governance
  • Goodtocode.SecuredHttpClient
  • Goodtocode.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:
    1. Quick-start for Microsoft Agent Framework -> product name
    2. GoodToCode Agent Framework -> company name plus product short name
    3. Goodtocode.AgentFramework -> namespace prefix (clone script only; templates already handle it)
    4. GoodToCode / Goodtocode / goodtocode (display, slug, domain, email forms) -> company
    5. goodtocode/agent-framework-quick-start -> repo owner/name (URLs, badges, clone and license links)
    6. 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 #if blocks in src.
  • Optional, low-risk cleanup that does not affect clone-and-run:
    • Normalize casing and wording so each brand role uses one spelling (Goodtocode for namespace, GoodToCode for display, one fixed product name), so the map matches reliably.
    • Centralize repeated display strings, for example in a Branding options 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.
  • 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 DotnetNew on .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 the Goodtocode.AgentFramework namespace rule.
  • template.json keeps sourceName: "Goodtocode.AgentFramework" and adds symbols (ProductName, CompanyName, ContactEmail, RepositoryUrl, Description, TenantHost) whose replaces target the new tokens. Use the XML, JSON and HTML encode forms where 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.ps1 additionally asserts that the generated output builds and contains no GoodToCode display text or tenant host outside the allow-lists.
  • Decide whether web-fullstack stays 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-ContentsByTag edits in Compress-Vstemplate-*.ps1, run the engine with -Target Vsix on .template.vsix/temp. It injects only non-namespace tokens: $guid1$ .. $guid3$ for the UserSecretsId values, and tokens for display text, emails, repo URLs and the tenant host, with $registeredorganization$ or $customparameters$ defaults where practical.
  • Leave unchanged: the $safeprojectname$ edits, the Presentation.Web.csproj exclusion, code namespaces, root.vstemplate and the Update-Vstemplate-*.ps1 drift checks.
  • Confirm each file that now carries a token has ReplaceParameters="true" (the existing Update-Vstemplate-*.ps1 fix 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 in App.razor together with the assembly name.
  • Solution files: rename Goodtocode.AgentFramework.Web.slnx, Goodtocode.AgentFramework.Api.slnx and the .slnLaunch.user file.
  • .github/**: workflows (gtc-agent-standalone-*.yml file names and the SRC_SLN value), dependabot.yml reviewers, copilot-instructions.md, instructions/*.md, repo scripts.
  • .azure/** (Bicep names and parameters) and .azuredevops/** (pipeline names, variables such as approver.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 the ai-seo-guide.md JSON-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.svg and optionally the favicon from a supplied path.
  • Regenerates the three UserSecretsId values (opt out with -KeepUserSecretsIds).

Behavior:

  • Takes parameters or a branding.json with: NewCompany, NewProduct, Namespace, Domain, ContactEmail, RepoOwner, RepoName, TenantHost, LogoPath, Copyright. The old values come from branding.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 .csproj and .sqlproj, YAML as text with a key-aware pass for name: and SRC_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 unmapped goodtocode / Microsoft Agent Framework quick-start text remains, apart from reviewed exceptions (the allow-lists and LICENSE text).
  • -Apply performs the changes. It refuses to run on a dirty git tree unless -Force is given.
  • After -Apply it prints the follow-up steps from the README flow: create the Entra app registrations and set the user-secrets for the new UserSecretsId values, and update WebRedirectUri and WebLogoutUri if 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 build succeeds, 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 src only.
  • No change to the existing steps, order or output of Build-DotnetTemplateRelease.ps1 and Build-VsixTemplateRelease.ps1 beyond the one added step.
  • No change to VSIX namespace handling or to the Presentation.Web.csproj exclusion.
  • 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 new template and the web and API VSIX still build and create building solutions.
  • branding.map.json is the only definition of the replacement rules, the dependency allow-list and the contract allow-list, consumed by Update-Branding.ps1 and by both template packaging scripts.
  • dotnet new agentweb -n Contoso.Chat --ProductName "Contoso Assist" --CompanyName Contoso builds, and its UI, Swagger and llms.txt show Contoso. The created Presentation.Api, Presentation.Web and Tests.Integration projects each have their own new UserSecretsId, and the committed appsettings contain no GoodToCode tenant host.
  • A VSIX-created solution builds, has new UserSecretsId values, 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 -WhatIf on a fresh clone reports changes with no unreviewed leftovers. -Apply produces a repo that builds, passes dotnet 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

  1. 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.
  2. quick-start: write Update-Branding.ps1 (engine with -Target Values|DotnetNew|Vsix), branding.sample.json and Pester tests.
  3. template (dotnet new): add the engine call to Update-DotnetTemplateWebContent.ps1 behind a switch, then the symbols, the generated GUID symbols and the leftover assertion in Test-DotnetTemplateWebCreate.ps1.
  4. template (VSIX): add the engine call and the leftover check to Compress-Vstemplate-*.ps1 and Build-VsixTemplateRelease.ps1 behind a switch, with $guid#$ and display tokens only. Add the before/after regression check.
  5. 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.ps1 and Compress-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 no Presentation.Web, so Web-only files, tokens and UserSecretsId values are neither expected nor flagged there.
  • The existing root.*.vstemplate copy/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.ps1 renames both .slnx files and the Web.slnLaunch.user file, and updates references to either in workflows, pipelines, .vscode and the README build commands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions