InfraPilot is an open-source infrastructure portal for self-service requests, approvals, deployments, and operational workflows.
If you just want to run the app locally, use Docker Compose.
- Docker
- Docker Compose
From the repository root:
docker compose up -dThen open:
- frontend:
http://localhost:5173 - API health endpoint:
http://localhost:5259/health
docker compose downIf you also want to remove the local Postgres volume:
docker compose down -vdocker compose up runs everything in containers, which is fine for a look around but slow to
iterate on. For development there are three scripts that keep Postgres in Docker and run the API and
the web dev server natively, so both reload on save:
./scripts/start.ps1Starts Postgres, applies migrations, seeds demo data into an empty database, and prints the URLs and
the dev sign-in accounts. Safe to re-run — anything already listening on its port is left alone.
-DbOnly starts just the database, for when the servers are launched from an IDE.
./scripts/reseed.ps1Drops the local database and rebuilds it: migrate, re-seed, then report the row counts. Seeding only
happens on an empty database, so this is the way back to a clean demo dataset. Destructive — it
prompts unless given -Force.
./scripts/stop.ps1Stops the servers and the database, keeping the volume. -KeepDb leaves Postgres up; -RemoveData
deletes the volume as well.
Logs and pid files go to .local/ (gitignored).
./scripts/tutorial/seed-tutorial.ps1Builds the environment used for presentations and training: the local database is dropped and
refilled with a copy of a real instance's active data — the current version of every service in
every environment, the last week of deploys, the registered builds and every open promotion — and
then a scripted storyline is staged on top (a promotion signed off and waiting for its release
approval, one with an issue raised, one rejected, one approved and waiting for its deploy, a failed
deploy with pipeline logs, a rollback request, a release note, a webhook subscription). Three
accounts come with it: admin@localhost / admin123 (Admin), qa@localhost / qa123
(QA) and user@localhost / user123 (plain User).
The copy is taken by scripts/tutorial/export-snapshot.ps1 through the public API, so it needs
DEPLOYMENTS_URL and DEPLOYMENTS_API_KEY for the source instance; the result lands in the gitignored
scripts/tutorial/snapshot/ and is reused by later runs (-RefreshSnapshot pulls again). Everything
else is written through the local API the way pipelines and users write it, so the data is exactly
what the product does with real payloads. The run ends with .local/tutorial-cheatsheet.md — the
promotion ids, links and curl commands the live parts of a demo need — and
docs/tutorial/presentation-guide.md is the matching
presenter's script. scripts/tutorial/webhook-listener.ps1 is a terminal that shows the seeded
webhook's deliveries arriving.
scripts/tutorial/capture (Node + Playwright) produces the offline version of the presentation: an
annotated screenshot walkthrough of every part, as PDF and HTML, from each actor's point of view, with
the terminal output of the live steps. npm run all there, after a fresh seed; output lands in
.local/tutorial-walkthrough/.
The script also writes src/Platform.Api/appsettings.Development.json (backing up an existing one):
the demo deployment seeder is switched off there (Seed:DemoDeployments), a tutorial-pipeline-key
API key is added for the curl commands, and the ingest rate limit is lifted. reseed.ps1 afterwards
gives a database with only the catalog and the users; delete the Seed block to get the built-in
demo dataset back.
./scripts/sync-mpt-versions.ps1 -WhatIfCatches InfraPortal up after an outage or a restore. Reads the version manifests MPT publishes for
staging and
production, confirms
each version against the AKS cluster behind the environment (mpt-staging-r1-aks,
mpt-prod-r1-aks), and records a manual deployment for every component whose version has moved
forward. It never creates services, never records an older version, and never writes a version the
cluster disagrees with; each of those is reported instead. Services are updated under the product
InfraPortal already files them under (mpt, or whatever an admin's service product override says),
never under the manifest's obsolete marketplace.
Only the drift is written, so re-running it changes nothing. Unlike the scripts above this one talks
to a real instance: connection comes from DEPLOYMENTS_URL and DEPLOYMENTS_API_KEY (or
-ApiBaseUrl / -ApiKey), and the cluster check needs kubectl credentials for both clusters
(az aks get-credentials) or -NoClusterCheck. -WhatIf prints what it would record, -Target staging|production limits it to one environment, and -Service 'mpt-web-*' to a subset of
components. Get-Help ./scripts/sync-mpt-versions.ps1 -Full has the rest.
docker compose starts three services:
postgres: PostgreSQL database onlocalhost:5433api: ASP.NET Core backend onhttp://localhost:5259frontend: React/Vite frontend onhttp://localhost:5173
The frontend is configured for local development through src/Platform.Web/public/config.json, which points API calls to http://localhost:5259.
For the default local Docker setup, you do not need to provide extra environment variables.
The local stack already uses:
- Postgres database:
swo_platform - Postgres user:
postgres - Postgres password:
postgres - API connection string:
Host=postgres;Database=swo_platform;Username=postgres;Password=postgres
If you want to change local settings, edit docker-compose.yml.
Production uses a single container image:
- Nginx serves the frontend on port
8080 - ASP.NET API runs inside the same container on
127.0.0.1:8081 - Nginx proxies
/api,/agent, and/healthto the API
Build it from the repository root:
docker build -t infrapilot .These are the most important environment variables for a real deployment.
| Variable | Required | Default | Why it is needed |
|---|---|---|---|
ConnectionStrings__Platform |
Yes | none | Tells the API how to connect to the database. The app cannot start correctly without a database connection. Format depends on Database__Provider (see below). |
Database__Provider |
No | Postgres |
Selects the EF Core provider. Accepted values: Postgres, SqlServer. Must match the format of ConnectionStrings__Platform. |
ASPNETCORE_ENVIRONMENT |
Recommended | Production in the container image |
Controls ASP.NET runtime behavior and environment-specific configuration. |
CatalogPath |
No | /app/catalog |
Tells the API where to load catalog YAML definitions from. |
These variables are used to generate config.json inside the container at startup.
| Variable | Required | Default | Why it is needed |
|---|---|---|---|
BACKEND_BASE_URL |
No | empty | Lets the frontend call a different public backend origin. Leave it empty for same-origin deployments. |
APP_NAME |
No | InfraPilot |
Sets the main product name shown in the UI. |
APP_SUBTITLE |
No | Infrastructure Portal |
Sets the smaller subtitle shown in the sidebar. |
ASSISTANT_NAME |
No | InfraPilot Assistant |
Sets the label used in the assistant/chat area. |
PAGE_TITLE |
No | InfraPilot | Infrastructure Portal |
Sets the browser tab title. |
AZURE_CLIENT_ID |
No | empty | Entra app (client) ID for SPA sign-in. Empty disables MSAL and falls back to a local dev user — use that for local runs only. |
AZURE_TENANT_ID |
No | empty | Entra tenant ID that hosts the SPA app registration. Required when AZURE_CLIENT_ID is set. |
MSAL config is read from /config.json at page load, so the same container image can be pointed at a different tenant by changing env vars at deploy time (no rebuild).
ASPNETCORE_ENVIRONMENT=Production
ConnectionStrings__Platform="Host=my-postgres;Database=infrapilot;Username=postgres;Password=secret"
CatalogPath=/app/catalog
APP_NAME="Contoso Platform"
APP_SUBTITLE="Operations Portal"
ASSISTANT_NAME="Contoso Assistant"
PAGE_TITLE="Contoso Platform | Operations Portal"
BACKEND_BASE_URL=""InfraPilot supports Azure SQL Database as an alternative to PostgreSQL. Switch by setting Database__Provider=SqlServer and using a SQL Server-format connection string. Both providers share the same schema — migrations for each set live under Migrations/Postgres and Migrations/SqlServer and are applied automatically on startup in Development.
Database__Provider=SqlServer
ConnectionStrings__Platform="Server=tcp:<server>.database.windows.net,1433;Database=infrapilot;Authentication=Active Directory Default;Encrypt=True;TrustServerCertificate=False;"Notes:
- Azure SQL requires
Encrypt=True. - Azure AD authentication (
Authentication=Active Directory Default) is recommended over SQL auth when running on Container Apps with managed identity. - On SQL Server, JSON payload columns are stored as
nvarchar(max)(on Postgres they usejsonb). The application serialises JSON itself so there is no behavioural difference.
You only need these if you want to enable the related features.
| Variable | Required | Why it is needed |
|---|---|---|
AzureAd__TenantId |
Only if using Entra ID auth | Identifies the Microsoft Entra tenant used for user authentication. |
AzureAd__ClientId |
Only if using Entra ID auth | Identifies the API application registration. |
AzureAd__Audience |
Only if using Entra ID auth | Defines the expected token audience for API auth validation. |
Graph__TenantId |
Only if using Microsoft Graph integration | Identifies the tenant for Graph client-credentials access. |
Graph__ClientId |
Only if using Microsoft Graph integration | Identifies the Graph app registration. |
Graph__ClientSecret |
Only if using Microsoft Graph integration | Secret used to authenticate to Microsoft Graph. |
AzureOpenAI__Endpoint |
Only if using AI features | Tells the app which Azure OpenAI resource to call. |
AzureOpenAI__ApiKey |
Only if using AI features | Authenticates requests to Azure OpenAI. |
AzureOpenAI__DeploymentName |
Only if using AI features | Selects the model deployment used by the app. |
AzureDevOps__Connections__default__OrganizationUrl |
Only if using Azure DevOps executors | Points to the Azure DevOps organization. |
AzureDevOps__Connections__default__Project |
Only if using Azure DevOps executors | Selects the Azure DevOps project used by the executor. |
AzureDevOps__Connections__default__Pat |
Only if using Azure DevOps executors | Personal access token used to call Azure DevOps APIs. |
Jira__Connections__default__BaseUrl |
Only if using Jira executors or lookups | Points to the Jira instance. |
Jira__Connections__default__Email |
Only if using Jira executors or lookups | Jira account email used for API authentication. |
Jira__Connections__default__ApiToken |
Only if using Jira executors or lookups | Jira API token used for authentication. |
AzureBlob__ConnectionString |
Only if using file attachments | Connects the app to Azure Blob Storage. |
AzureBlob__ContainerName |
Only if using file attachments | Selects the blob container for uploaded files. |
ServiceBus__ConnectionString |
Only if using Service Bus execution flow | Connects the app to Azure Service Bus. |
ServiceBus__ExecutionQueueName |
Only if using Service Bus execution flow | Selects the queue used for request execution. |
Notifications__PortalBaseUrl |
Recommended if using email/webhook notifications | Lets notifications link users back to the correct portal URL. |
Notifications__Channels__Email__Enabled |
Only if using email notifications | Enables or disables the email channel. |
Notifications__Channels__Email__SmtpHost |
Only if using email notifications | SMTP server hostname for sending emails. |
Notifications__Channels__Email__SmtpPort |
Only if using email notifications | SMTP server port. |
Notifications__Channels__Email__From |
Only if using email notifications | Sender address used in notification emails. |
Notifications__Channels__Email__UseSsl |
Only if using email notifications | Enables SSL/TLS for SMTP. |
Notifications__Channels__Webhook__Enabled |
Only if using outbound notification webhooks | Enables or disables the webhook channel. |
Notifications__Channels__Webhook__Url |
Only if using outbound notification webhooks | Destination URL for generic webhook notifications. |
APPLICATIONINSIGHTS_CONNECTION_STRING |
Only if using Application Insights | Enables Azure Monitor / Application Insights telemetry (distributed tracing, dependency tracking, live metrics). When empty or absent, OpenTelemetry is not registered and there is zero overhead. |
Deployments__Enrichment__Enabled |
Only if using deployment enrichment | Turns background deployment enrichment on or off. |
Deployments__Enrichment__IntervalSeconds |
Only if using deployment enrichment | Controls how often enrichment runs. |
Deployments__Enrichment__MaxEventsPerCycle |
Only if using deployment enrichment | Limits how many deployment events are processed per cycle. |
Deployments__Enrichment__LookbackHours |
Only if using deployment enrichment | Controls how far back the enrichment worker searches for events. |
InfraPilot uses two Entra ID app registrations: one for user-facing authentication (SPA + API) and one for server-to-server Graph API calls. Without auth configured the app runs in open development mode — all endpoints are accessible and a stub dev user with admin rights is injected automatically.
This is the main app registration used by both the React frontend (MSAL) and the ASP.NET API (JWT validation).
- Go to Azure Portal > Microsoft Entra ID > App registrations > New registration
- Name:
InfraPilot(or your preferred name) - Supported account types: Single tenant
- Redirect URI: Single-page application (SPA) —
http://localhost:5173for dev, add your production URL later
- Go to Expose an API
- Set the Application ID URI to
api://<client-id>(the default) - Add a scope:
- Scope name:
access_as_user - Who can consent: Admins and users
- Admin consent display name:
Access InfraPilot as user
- Scope name:
- The frontend requests this scope when acquiring tokens:
api://<client-id>/access_as_user
Go to App roles > Create app role and add:
| Display name | Value | Allowed member types | Description |
|---|---|---|---|
InfraPilot Admin |
InfraPortal.Admin |
Users/Groups | Full admin access — catalog sync, audit log viewer |
The role value must be exactly InfraPortal.Admin (case-sensitive). This role controls:
| What it unlocks | Where it's enforced |
|---|---|
Catalog sync trigger (POST /api/catalog/sync) |
Backend policy CatalogAdmin |
Audit log access (GET /api/audit) |
Backend policy AuditViewer |
| "Admin" badge in sidebar | Frontend user.isAdmin check |
Users without this role can still browse the catalog, submit requests, approve requests, and view deployments.
- Go to Enterprise applications > find
InfraPilot> Users and groups - Click Add user/group
- Select users or a security group, then select the
InfraPilot Adminrole - Click Assign
To include group memberships in tokens (used for approval routing):
- Go to Token configuration > Add groups claim
- Select Security groups
- This populates the
groupsclaim in the JWT, which the backend reads viaCurrentUser.Groups
AzureAd__TenantId=<your-tenant-id>
AzureAd__ClientId=<client-id-from-above>
AzureAd__Audience=api://<client-id-from-above>Set these in src/Platform.Web/.env (or as build-time vars):
VITE_AZURE_CLIENT_ID=<client-id-from-above>
VITE_AZURE_TENANT_ID=<your-tenant-id>If these are empty or start with <, MSAL is disabled and the app falls back to the stub dev user.
A separate app registration used by the backend to call Microsoft Graph with client credentials. This is needed for resolving approval group members (e.g. looking up who belongs to platform-infra-approvers).
- App registrations > New registration
- Name:
InfraPilot Graph - Supported account types: Single tenant
- No redirect URI needed
- Go to API permissions > Add a permission > Microsoft Graph > Application permissions
- Add:
GroupMember.Read.All— read group membershipsUser.Read.All— read user profiles
- Click Grant admin consent
- Go to Certificates & secrets > New client secret
- Copy the secret value immediately (it won't be shown again)
Graph__TenantId=<your-tenant-id>
Graph__ClientId=<graph-app-client-id>
Graph__ClientSecret=<secret-from-above>If these are empty or start with <, the backend falls back to StubIdentityService which returns an empty member list for all groups.
Catalog YAML items reference Entra ID security groups by name in the approver_group field:
approval:
required: true
strategy: any
approver_group: "platform-infra-approvers"When a request needs approval, the backend calls IIdentityService.GetGroupMembers(groupId) to resolve who can approve. In production this uses Graph API; in development it returns a stub list.
| Registration | Used by | Purpose | Required for |
|---|---|---|---|
| InfraPilot (SPA + API) | Frontend + Backend | User login, JWT validation, role-based access | Auth in production |
| InfraPilot Graph | Backend only | Resolve group members via Graph API | Approval routing |
Without either registration configured, the app runs fully functional in open development mode.
Pipelines post deployment events to POST /api/deployments/events with the header X-Api-Key: <key>. Keys are configured via Deployments:ApiKeys:
# Minimum — plaintext key (fine for dev, OK for prod if secrets are in a vault)
Deployments__ApiKeys__0__Name=azure-devops-pipeline
Deployments__ApiKeys__0__Key=dpk-a1b2c3d4e5f6g7h8i9j0-ado
# Production — hashed key; restricted to specific products; revocable
Deployments__ApiKeys__1__Name=github-actions-platform
Deployments__ApiKeys__1__KeyHash=9a86f1a7e8c6... # lowercase SHA-256 hex of the real key
Deployments__ApiKeys__1__AllowedProducts__0=platform
Deployments__ApiKeys__1__AllowedProducts__1=billing
Deployments__ApiKeys__1__Revoked=falseHardening:
KeyHash(preferred for prod) — storesha256(key)instead of the raw key. Generate withprintf '%s' 'dpk-...' | shasum -a 256. IfKeyHashis set,Keyis ignored.AllowedProducts— restrict a key so it can only post events for specific products. Empty list = any product. Requests for other products get403 Forbidden.Revoked— set totrueto instantly kill a key without removing the entry (keeps audit history aligned).- Rate limit — each authenticated key is limited to 120 requests/minute (sliding window). Unauthenticated callers get a stricter shared 10/min bucket. Excess returns
429. - Constant-time compare — both plaintext and hash comparisons use
CryptographicOperations.FixedTimeEqualsto resist timing attacks. - HTTPS — keys travel in a header, so always terminate TLS before the API. Never expose
/api/deployments/eventsover plain HTTP.
A deployment event is a small JSON document. Only product, service, environment, version, source, and deployedAt are required; everything else is optional enrichment that the UI uses to render richer cards and links.
Reference types the UI recognises with a dedicated icon and label:
type |
Icon | Label preference |
|---|---|---|
work-item |
work item | key (e.g. PLAT-1234) — shows the inbound title when supplied, otherwise the Jira title fetched server-side |
pull-request |
PR | labels.prTitle → key |
repository |
branch | key (e.g. acme/platform-api) → parsed from url → short revision |
pipeline |
workflow | key → provider |
Unknown types render with a generic external-link icon. Always include url when you have it — the UI turns the label into a link.
Commit deep-linking. A repository reference that includes both url and revision is rendered as a link directly to that commit, derived from the provider:
| Provider | Resolved URL |
|---|---|
github, azure-devops |
{url}/commit/{revision} |
gitlab |
{url}/-/commit/{revision} |
bitbucket |
{url}/commits/{revision} |
| other / omitted | falls back to url |
So a payload like { "type": "repository", "provider": "github", "url": "https://github.com/acme/platform-api", "revision": "a1b2c3d4" } deep-links to https://github.com/acme/platform-api/commit/a1b2c3d4. No org/repo names are hardcoded — the URL is derived purely from the inbound url.
Minimal curl example:
curl -X POST "$PLATFORM_URL/api/deployments/events" \
-H "X-Api-Key: $DEPLOY_KEY" \
-H "Content-Type: application/json" \
-d '{
"product": "platform",
"service": "platform-api",
"environment": "production",
"version": "2.4.1",
"source": "github-actions",
"deployedAt": "2026-04-15T09:12:00Z",
"references": [
{ "type": "repository", "url": "https://github.com/acme/platform-api", "provider": "github", "key": "acme/platform-api", "revision": "a1b2c3d4" }
]
}'previousVersion is computed automatically by the server from the last event for the same (product, service, environment) tuple — publishers never send it. Set isRollback: true when the new version is a re-deploy of a prior version (the UI then renders an Undo2 icon next to the version with a Rolled back from v{previousVersion} tooltip).
Release Notes turn a stream of DeployEvents into structured, human-readable summaries — one note per (product, environment, window) — and broadcast them as a webhook so downstream consumers (Teams, Confluence, an email blast) can publish without a second call back to InfraPilot.
The feature is gated by the features.releaseNotes flag and is off by default. Flip it on per environment from Settings → Feature Flags.
| Step | Endpoint | Persists | Webhook |
|---|---|---|---|
| 1. Raw aggregation | GET /api/release-notes/preview/raw |
no | no |
| 2. Templated preview | GET /api/release-notes/preview |
no | no |
| 3. Publish | POST /api/release-notes/generate |
yes | yes (release_note.generated) |
| 4. List | GET /api/release-notes |
— | — |
| 5. Detail | GET /api/release-notes/{id} |
— | — |
In the UI, the list page (/release-notes/:product) shows the form for picking environment + window. The "Preview" button navigates to a dedicated draft route (/release-notes/:product/new?env=…&from=…&to=…) where the rendered markdown can be edited side-by-side with a live HTML preview. "Publish" persists the (possibly edited) markdown and redirects to the permanent detail URL.
Release notes are rendered with Handlebars.Net against the aggregated services. Templates are stored in platform_settings at one of three scopes; resolution picks the most-specific row that exists:
| Scope | platform_settings.Key |
|---|---|
| Per (product, environment) | release-notes.template.{product}.{environment} |
| Per product | release-notes.template.{product} |
| Global default | release-notes.template.default |
Edit via Settings → Release Notes Template (admin only). The editor loads the exact row for the chosen scope; if nothing is saved there yet it shows the inherited template so you can fork it.
Top level: product, environment, date, from, to, services (array).
Per service (inside {{#each services}}): service, previousVersion, currentVersion, isRollback, deployedAt, workItems[], pullRequests[], pipelines[], participants[], plus the first-match shortcuts pullRequest, pipeline, author, qa, triggeredBy (each { displayName, email }).
Use {{{name}}} (triple-mustache) for content that should not be HTML-escaped (e.g. display names with diacritics).
The release_note.generated event fires once a note is persisted and is subject to the standard webhook subscription filters (Product, Environment):
{
"id": "ae1fa7ef-...",
"product": "identity-platform",
"environment": "production",
"from": "2026-05-06T21:12:17Z",
"to": "2026-05-07T14:00:00Z",
"generatedAt": "2026-05-07T14:05:00Z",
"renderedContent": "# 🛠️ Release: identity-platform — production\n...",
"services": [
{
"service": "auth-api",
"previousVersion": "1.8.5",
"currentVersion": "1.10.0",
"isRollback": false,
"workItems": [{ "key": "IDP-2946", "title": "...", "url": "..." }],
"pullRequests": [{ "key": "888", "title": "...", "url": "..." }],
"pipelines": [{ "key": "build-79588", "url": "..." }],
"participants": [{ "role": "author", "displayName": "...", "email": "..." }]
}
]
}Because renderedContent is included, a Logic App or Azure Pipeline can post directly to Teams without calling back to InfraPilot.
A parallel event release_note.generated.html fires with the same payload plus a renderedHtml field (rendered server-side by Markdig). Subscribe to whichever event suits the downstream — markdown subscribers (Teams, Slack) stay on release_note.generated; HTML consumers (Confluence storage format, HTML email templates, SharePoint pages) subscribe to release_note.generated.html. Markdown subscribers don't pay the size cost of the HTML payload.
Do not commit real secrets to the repository.
Keep these out of source control:
Graph__ClientSecretAzureOpenAI__ApiKeyAzureDevOps__Connections__*__PatJira__Connections__*__ApiTokenDeployments__ApiKeys__*__Key
For real deployments:
- use environment variables or a secret store
- use Azure Container Apps secrets if deploying to Azure
- rotate any secrets that were previously committed or shared
The simplest recommended Azure setup is:
- one Azure Container App
- external ingress on port
8080 - one custom domain
- managed PostgreSQL outside the container
This gives you:
- one deployment unit
- same-origin frontend and API
- no extra routing layer
This repository uses two GitHub Actions workflows:
- CI workflow
Validates that the single production image builds successfully on pull requests and pushes to
main/master. - Release workflow
Publishes the production image to
ghcr.io/<owner>/<repo>when you push a Git tag likev0.1.0.
Example release flow:
git tag v0.1.0
git push origin v0.1.0That publishes image tags such as:
ghcr.io/<owner>/<repo>:v0.1.0ghcr.io/<owner>/<repo>:latest
src/
Platform.Api/
Platform.Web/
catalog/
guides/ assistant walkthroughs (GUIDES_PATH)
knowledge/ assistant reference topics (KNOWLEDGE_PATH)
playbooks/ assistant failure causes (PLAYBOOKS_PATH)
infra/
docs/
Dockerfile
docker-compose.yml
Three YAML corpora feed the in-app assistant. All are loaded once at startup, ship in the image, and are validated by tests — a broken reference fails the build rather than degrading an answer silently.
| Directory | Answers | Shape |
|---|---|---|
guides/ |
"How do I roll back?" | Ordered steps, each optionally naming a data-guide-anchor the UI spotlights |
knowledge/ |
"What does this webhook do?", "Why does prod need sign-off?" | Markdown prose with source and as_of attribution |
playbooks/ |
"Why is this promotion stuck?" | Causes selected by observation flags the diagnostics compute from live state |
Write a YAML file in guides/ with id, title, route, and steps. To point at a control, add
data-guide-anchor="some-name" to the element and name it in the step's anchor. Nav items are
anchored automatically as nav-<route>. GuideCorpusTests fails if an anchor, route or related
id does not exist.
Every chat turn carries the page the user has open: its name and state (published by the page's
HelpButton, or by calling usePageContext directly), and the list of every data-guide-anchor
in the DOM. The assistant is instructed to treat that as the primary referent — "how do I approve
that?" means the record on screen, not the one the conversation was about — and to ring what it
talks about with its highlight tool, which only accepts anchors from that list.
To make something pointable, give it a data-guide-anchor. Fixed controls take a fixed name
(promotion-approve-button); elements whose identity is data follow a pattern the server also
knows (PortalAnchors): service-row:<service>, env-cell:<service>:<environment>,
env-column:<environment>, promotion-row:<id>. Asking for a service's version navigates to its
page and rings those cells without the model having to ask.
Write a YAML file in knowledge/ with id, title, summary, body (Markdown), plus source and
as_of. Those last two are required: these topics describe pipelines in other repositories
(mpt-release, marketplace, ops-build-templates-aks-releases), so every claim has to be
traceable and dated. Add aliases for how people actually phrase the question — that is what the
keyword search ranks on.
Write a cause under the relevant playbooks/ file with matches_when listing observation flags from
Observations. The diagnostics compute those flags from live state and only offer causes whose flags
hold, so the assistant cannot invent an explanation. A cause keyed on a flag nothing emits is dead —
DiagnosticsCorpusTests fails the build for exactly that.
To make a new condition diagnosable: add the flag to Observations, emit it in DiagnosticsService,
and write the cause that uses it.
Check that:
- the API container is running:
docker compose ps - the API health endpoint responds:
http://localhost:5259/health src/Platform.Web/public/config.jsonstill points tohttp://localhost:5259
This project maps Postgres to localhost:5433 to avoid conflicts with a local 5432 instance.
docker compose down -v
docker compose up -d
{ "product": "platform", "service": "platform-api", "environment": "production", "version": "2.4.1", "source": "azure-devops", // free-form tag (e.g. pipeline name) "deployedAt": "2026-04-15T09:12:00Z", "status": "succeeded", // "succeeded" | "failed" | "in_progress" "isRollback": false, // set true when this deploy reverted to a prior version "references": [ { "type": "pull-request", "url": "https://github.com/acme/platform-api/pull/482", "provider": "github", "key": "482", "title": "Add idempotency key to checkout" }, { "type": "work-item", "url": "https://acme.atlassian.net/browse/PLAT-1234", "provider": "jira", "key": "PLAT-1234", "title": "Add idempotency key to checkout endpoint" }, { "type": "repository", "url": "https://github.com/acme/platform-api", "provider": "github", "key": "acme/platform-api", "revision": "a1b2c3d4" }, { "type": "pipeline", "url": "https://dev.azure.com/acme/_build/results?buildId=98765", "provider": "azure-devops", "key": "98765" } ], "participants": [ { "role": "PR Author", "displayName": "Sylwester Grabowski", "email": "sg@acme.com" }, { "role": "PR Reviewer", "displayName": "Alex Kim", "email": "ak@acme.com" }, { "role": "QA", "displayName": "Jordan Lee", "email": "jl@acme.com" } ], "metadata": { "runId": "20260415.1", "releaseNotes": "hotfix for auth cache" } }