Skip to content

ADR 0004 — BMO lives in this repository#

Status Accepted
Date 2026-09-21
Authors Ruben Knuijver
Supersedes
Superseded by
Issue #286

Context#

BMO (Benefits.CommandCenter, Benefits.Agent, the bmo CLI) installs and manages BenefitManager versions on IIS and Windows Services. It is in production, and its API and CLI tooling are more advanced than CommandCenter's equivalents — the Fleet manager, stage removal and deploys in this repository already proxy to it or are slated to replace their legacy equivalents with it.

It lived in github.com/bcs-bv/benefits-mgmt-agent. The next BMO releases depend on CommandCenter changes, which means the two repositories would have had to be released in lockstep across two hosts with two CI systems. They also already share a SQL schema without declaring it: BMO reads and writes TblCustomerStage, TblServerLog, TblDeployLog and TblToolSchedule with raw Dapper, while CommandCenter.WebApi's EF migrations are what create those tables.

Decision#

Import the whole BMO repository under the top-level prefix bmo/, with full history, using git subtree add without --squash. Deployments from benefits-mgmt-agent are frozen; there is no two-way sync to maintain.

Why the whole repository, not a subtree split of src/#

BMO's build files live at its root, and Benefits.CommandCenter.csproj declares no TargetFramework — it inherits net10.0 from BMO's Directory.Build.props. src/ without that file does not build. Importing everything keeps history for everything; deleting what has no job here afterwards is an ordinary commit.

Why a top-level prefix#

MSBuild resolves the nearest Directory.Build.props / Directory.Packages.props walking up — nearest wins outright, with no merging. With BMO's own files at bmo/, they shadow ours completely and BMO keeps its target frameworks, its AnalysisModeSecurity=Recommended (ours is All), its MinVer versioning (ours is Nerdbank.GitVersioning), its own 129-entry package list and its own nuget.config. Nesting under backend/ would have put a second repository inside CommandCenter's project tree and inside .claude/rules/backend.md's scope, whose content would then be wrong for every BMO file.

Verified after the import: Benefits.CommandCenter.csproj evaluates to TargetFramework=net10.0, AnalysisModeSecurity=Recommended, and no Nerdbank assembly appears in BMO's build output.

Why BMO is not in CommandCenter.sln#

Benefits.Agent, Benefits.Agent.ServiceBus and three of the four test projects target net10.0-windows; Benefits.Agent and Benefits.Orchestrator pin RuntimeIdentifier=win-x64. Our only .NET CI job runs dotnet test CommandCenter.sln on mcr.microsoft.com/dotnet/sdk:10.0, where Windows test executables cannot run — the same limit already carved out for CommandCenter.SystemStatsAPI. BMO builds as bmo/benefits-mgmt.slnx and is gated separately by bmo:build.

Versioning#

BMO's 18 release tags were fetched renamed, v*bmo-v*, and MinVerTagPrefix in bmo/Directory.Build.props was changed to match. Because a non-squash git subtree add merges BMO's real commits, those tags point at commits that genuinely are in this repository's history, so version continuity is preserved without adopting Nerdbank for BMO. Verified: MinVer reports 2.3.1-alpha.0.15, derived from bmo-v2.3.0, not from our v2.1.

What was pruned, moved and kept#

Removed (ordinary deletions; history is preserved and git blame still reaches them): bmo/frontend (a scratch react-example Vite app in no solution and no CI), bmo/CLAUDE.md (cross-contaminated from this repository — it described two EF DbContexts, a MassTransit hook and a vite proxy, none of which BMO has), bmo/.claude, bmo/.husky, bmo/mkdocs.yml, bmo/Dockerfile.mkdocs, bmo/repomix.config.json, bmo/.repomixignore, and the seven bmo/.ai/skills directories that were byte-identical to ours.

Moved: bmo/docs/{index.md,guides,architecture}docs/bmo/; bmo/.plans, bmo/.compliance and bmo/docs/superpowersdocs/bmo/history/ (design record, and the compliance notes are the only written statement of the AI Act / CRA / GDPR / NIS2 obligations); bmo/readme.mdbmo/README.md; BMO's six unique .ai/skills up to .ai/skills/.

Kept: bmo/AGENTS.md, bmo/CHANGELOG.md, bmo/README.md, bmo/scripts, bmo/deploy.ps1, bmo/Register-BMO.ps1, bmo/local-dev, bmo/docker-compose.dev.yml, bmo/.gitleaks.toml, bmo/.dockerignore, bmo/.gitignore. bmo/.github is kept for now so that A-2's port of those workflows to GitLab CI reads as a move rather than a deletion plus an unrelated addition.

Consequences#

Two pre-existing BMO defects surfaced immediately#

Both were invisible while the image was pulled ready-made rather than built. They are the clearest evidence the import was worth doing.

  1. The Dockerfile could not restore. bmo/src/Benefits.CommandCenter/Dockerfile copies Directory.Build.props but never Directory.Packages.props, while BMO sets ManagePackageVersionsCentrally and every PackageReference is versionless. Restore failed with NU1015 across every project. Fixed with one COPY line.
  2. The healthcheck could never have passed. compose.yaml probed https://localhost:8090/healthz with wget. The aspnet:10.0 base image ships neither wget nor curl, and with no server certificate configured BMO logs "binding plain HTTP on :8090" — so the URL scheme and the tool were both wrong. It now uses bash and /dev/tcp (CMD, not CMD-SHELL: CMD-SHELL runs dash, which has no /dev/tcp).

History access is asymmetric#

git subtree add grafts BMO's tree under the prefix at the merge commit but does not rewrite paths in the 148 imported commits — they still say src/…, and pathspec filtering cannot cross the graft. Therefore:

  • git blame bmo/<file> works, and follows BMO's own src/BenefitManager.*src/Benefits.* rename.
  • git log -- bmo/<file> shows only the import merge. Path history comes from querying the BMO side with its original root-relative path: git log --follow -- src/Benefits.CommandCenter/Services/StageRemovalExecutor.cs.

Rewriting paths with git filter-repo --to-subdirectory-filter would have fixed git log at the cost of changing every SHA, which would have broken the tag-to-commit identity that the MinVer continuity above depends on. Blame is the archaeology that actually gets used; the trade was accepted.

The import merge commit must never be squashed or rebased. This project has squash_option: default_on, so the squash checkbox is pre-checked on every merge request and has to be unchecked for the import.

Tag hygiene#

Git's automatic tag following brings BMO's original v* tag names along even when the fetch refspec renames them. After the import, 18 unprefixed v* tags had to be deleted locally, leaving only CommandCenter's own v2.0 and v2.1. remote.bmo.tagOpt is set to --no-tags. Do not git push --tags after fetching from a BMO remote.

global.json does not isolate#

Unlike Directory.*.props, global.json resolves from the invocation directory upward. A build started at the repository root uses our 10.0.201 pin with rollForward: latestFeature, and BMO's {"test": {"runner": "Microsoft.Testing.Platform"}} is ignored. Harmless for the compile gate; it matters when BMO's test jobs land in A-2.

CI and compose#

bmo:build runs dotnet build bmo/benefits-mgmt.slnx -c Release on the existing Linux SDK image when anything under bmo/** changes, with GIT_DEPTH: 0 because MinVer needs the tags. Verified by running the job's own commands inside mcr.microsoft.com/dotnet/sdk:10.0 against a git archive of the branch: 16 projects, 0 errors — every net10.0-windows and win-x64 project compiles on Linux.

commandcenter-bmo now builds from ./bmo and the BFF's cluster-bmo destination plus BMO_BASE_URL default to http://commandcenter-bmo:8090/. Verified: the container builds and reaches healthy. The end-to-end leg — the SPA Fleet dashboard rendering against it — is not verified, because BMO needs ConnectionStrings__CommandCenter_* keys the dev stack does not provide; that is why the bmo profile stays opt-in.

What is deliberately not done here#

  • The release pipeline (A-2). BMO's three GitHub Actions workflows still sit in bmo/.github and are dead where they are. Porting them means: bmo:test:linux (Testcontainers + Azurite on dind), bmo:test:windows on the BEN-APP01 runner (whether it has the .NET 10 SDK is unverified — it is tagged vs2019), bmo:pack for Benefits.Contracts + Benefits.Agent.Client, bmo:release on tag bmo-v* with cross-published win-x64 self-contained zips (which need no Windows host), and release:image:bmo. The compliance gate is not ported one-to-one: this repository already includes GitLab's Secret-Detection and SAST templates.

Note for that work: bmo/scripts/build-commandcenter-image.ps1 derives its default version from git describe --tags --dirty with a leading v stripped. With the prefix now bmo-v, that produces a wrong version string.

Who consumes the two NuGet packages is not established — no consumer exists in any local repository. Until one is identified the publish target stays GitHub Packages.

  • Unification (A-3). Folding BMO into CommandCenter.sln and this repository's MSBuild roots is a project-by-project job (RecommendedAll analyzer fixes, merging the package lists, an explicit TargetFramework on Benefits.CommandCenter, MinVer → Nerdbank with pathFilters). Also deferred: moving BMO into the committed-spec OpenAPI pipeline and retiring gen:bmo's live URL; ~~retiring the BmoPackageReader bridge in favour of a direct reference to Benefits.PackageStore~~ (done — CommandCenter.WebApi references Benefits.PackageStore and Benefits.Agent.Client directly; see backend/CLAUDE.md → BMO libraries in WebApi); replacing BMO's hand-rolled Serilog and auth duplicates with Shared.*; making bmo stage remove call CommandCenter's endpoint so there is one stage-removal implementation again.

  • Rotating the committed API key. bmo/src/Benefits.CommandCenter/appsettings.Production.json carries ClientAuthentication.ApiKey: "123-abcd" and AgentApiKey: "123-abcd". It is already in BMO's history and GitLab Secret Detection will now scan it. Rotation is tracked separately.

  • BMO's unmerged branches. Seven branches were unmerged into BMO main at import time. feature/installed-versions-cleanup had no commits not already in main. feature/cli-interactive-config (10 commits, the bmo config wizard) had had its upstream deleted and was pushed back to origin before the import so it is not machine-local. The remaining five have live upstreams and must be recreated here if wanted; they are lost when benefits-mgmt-agent is archived. stash@{0} was a three-line edit to repomix.config.json, a file this import deletes.

  • ADR 0003 — BMO deploy history is a separate view. Its premise, that changing BMO's contract is "BMO-repo work, out of scope", no longer holds and it should be revisited.
  • .claude/rules/bmo.md — the working rules for bmo/.
  • docs/bmo/ — BMO's operator guides and design history.