Prerequisites and first deploy — start here¶
This is the one authoritative page for standing up CSA Loom: what you must prepare, how to deploy, and how to confirm each step actually worked. Everything else in this section is depth you reach for when you need it — this page is the spine.
It exists because of a specific complaint: the deploy should configure everything for the user, and where prework genuinely is required it must be called out, clear, and step by step with all the information needed to do it. Those are two different obligations, and this page keeps them separate.
- What the platform can do, the platform does. Per
auto-bind-by-default.md§5, infrastructure prerequisites are deployed, not requested. If you find a step here that tells you to go set a value the deploy could have produced, that is a defect to file, not a chore to accept. - What genuinely cannot be automated is listed below, in full, with the exact command and a verification for each.
The short version¶
Three things block a first deploy. Everything else is deployed for you.
| # | Prerequisite | Why it cannot be automated | Time |
|---|---|---|---|
| 1 | An Azure subscription and the rights on it | It is the entry condition. Nothing can grant itself a subscription. | 0–1 day (procurement) |
| 2 | A deployment identity, as GitHub secrets | Chicken-and-egg: the identity that performs the deploy cannot deploy itself. | ~10 min |
| 3 | An admin binding — who gets /admin/* | A policy decision about which humans are administrators. The platform cannot invent it. | ~2 min |
A further set of tenant consents is required only for specific feature surfaces — MIP/DLP, cross-subscription Connections, Power Platform. None of them block the deploy or sign-in, and each is listed in post-deploy tenant consents.
If you only read one thing
Set FIAB_TENANT_ADMIN_OID to your own Entra user object id. It is the single most important value on this page, it is the one binding that has never depended on a claim that might not be emitted, and until now it was documented nowhere.
What the platform does for you¶
Listed so you do not go looking for work that is already done. Every row below is performed by the deploy or the post-deploy bootstrap — you do not do any of it.
-
Backing services and their bindings
Every Azure backend Loom uses — ADLS, Synapse, Databricks, ADX, Event Hubs, Cosmos, AI Search, AOAI/Foundry, Key Vault, APIM, Purview — is provisioned and bound, with names derived deterministically from the estate. You never hand-map a Loom item to its Azure resource.
-
Every
LOOM_*environment variableRendered onto the Container Apps by the deploy. There is no list of env vars for you to set. A capability that reports Blocked because a value the deploy could have set was not set is a defect — file it.
-
Data-plane grants Bicep cannot make
The post-deploy bootstrap grants the Console identity Synapse Administrator, registers it with Databricks via SCIM, wires APIM, assigns Microsoft Graph application roles, grants Foundry and Content Safety roles, and registers the Power Platform management app.
-
The sign-in app registration
The MSAL app registration is created, its redirect URIs set, its client secret written to Key Vault,
groupMembershipClaims=SecurityGroupconfigured, andLOOM_MSAL_CLIENT_IDwired onto the Console. -
Brownfield discovery
A read-only multi-subscription inventory of what you already have, with a per-service adopt / create / skip decision. Loom never silently adopts and never silently duplicates.
-
Seed state
Cosmos containers, the AI Search governance catalog index, default Monitor alert rules and the action group — all created on first deploy so the surfaces resolve day-one rather than 404.
Part 1 — the prerequisites, in full¶
P1 — a subscription and the rights on it¶
What it is. One Azure subscription for a single-subscription install; one for the Admin Plane plus one per Data Landing Zone for multi-subscription.
What you need on it. Owner, or Contributor + User Access Administrator. The second role is not optional decoration — the deploy writes RBAC role assignments, and without Microsoft.Authorization/roleAssignments/write it fails partway through having created resources it cannot wire.
How to confirm it took:
az login
az account set --subscription <subscription-id>
# Expect Owner, or both Contributor and User Access Administrator.
az role assignment list \
--assignee "$(az ad signed-in-user show --query userPrincipalName -o tsv)" \
--scope "/subscriptions/$(az account show --query id -o tsv)" \
--query "[].roleDefinitionName" -o tsv
Also check quota before you start, because quota failures surface 40 minutes into a deploy rather than at the front:
az vm list-usage --location <region> -o table # Container Apps / ACR task families
az account list-locations --query "[?name=='<region>']" -o table
Azure OpenAI capacity (TPM) is requested through the portal Quotas blade and can take a business day. Request it before you begin.
P2 — the deployment identity¶
What it is. A service principal in the target tenant that the deploy workflows authenticate as. This is the genuine chicken-and-egg: the identity that creates everything cannot create itself, so this one step is yours.
Create it once per tenant:
SUB_ID=$(az account show --query id -o tsv)
# Creates the SP and prints appId + password. Capture both — the password is
# shown ONCE and cannot be retrieved again.
az ad sp create-for-rbac \
--name "csa-loom-deploy" \
--role Contributor \
--scopes "/subscriptions/$SUB_ID" \
--output json
Then add the role that lets it write RBAC:
SP_OBJECT_ID=$(az ad sp list --display-name "csa-loom-deploy" --query "[0].id" -o tsv)
az role assignment create \
--assignee-object-id "$SP_OBJECT_ID" \
--assignee-principal-type ServicePrincipal \
--role "User Access Administrator" \
--scope "/subscriptions/$SUB_ID"
Confirm the SP has what it needs (expect both roles listed):
az role assignment list --assignee "$SP_OBJECT_ID" \
--scope "/subscriptions/$SUB_ID" --query "[].roleDefinitionName" -o tsv
Store the credentials as GitHub secrets¶
Never put a secret value on a command line
gh secret set NAME --body "<value>" writes the value into your shell history and into any process listing. Omit --body — gh then prompts and reads from stdin, and the value never touches the history file.
# Each of these prompts: "Paste your secret:" — paste, then press Enter.
gh secret set AZURE_CLIENT_ID
gh secret set AZURE_CLIENT_SECRET
gh secret set AZURE_TENANT_ID
gh secret set AZURE_SUBSCRIPTION_ID
Confirm they took. gh secret list prints names and update timestamps and never prints a value — which is exactly why it is the right verification:
gh secret list | grep -E 'AZURE_(CLIENT_ID|CLIENT_SECRET|TENANT_ID|SUBSCRIPTION_ID)'
# Expect 4 rows. A missing row is the whole failure — there is no partial state.
The end-to-end proof that the credentials actually authenticate is the whatif-only dispatch in verify the credentials.
P3 — the admin binding¶
What it is. Which humans may open /admin/*. There are two independent bindings and you should set both:
| Binding | Where it lives | What it is |
|---|---|---|
FIAB_TENANT_ADMIN_OID | repo variable | A single Entra user object id — a bootstrap admin who bypasses the feature-permission gate. |
FIAB_ADMIN_GROUP_ID | repo secret or variable | An Entra security group object id whose members are admins. |
Why both, and why the OID first
The group binding depends on Entra emitting a groups claim, which in turn depends on the bootstrap having successfully set groupMembershipClaims=SecurityGroup on the app registration. That path is wired today (the callback reads the claim, and scripts/csa-loom/bootstrap-msal-app-reg.sh sets the property), but it has more moving parts than the OID binding, which is a direct object-id compare.
The deploy refuses to render the Container Apps when neither is resolvable — a Console with both empty has an admin gate nobody can pass and no in-product way out. Setting the OID is the cheapest insurance against that outcome.
Get the two values:
# Your own object id — this is FIAB_TENANT_ADMIN_OID.
az ad signed-in-user show --query id -o tsv
# Create the admin group and put yourself in it — the output is FIAB_ADMIN_GROUP_ID.
az ad group create --display-name "Loom Admins" --mail-nickname "loom-admins" -o none
GROUP_ID=$(az ad group show --group "Loom Admins" --query id -o tsv)
az ad group member add --group "$GROUP_ID" \
--member-id "$(az ad signed-in-user show --query id -o tsv)"
echo "$GROUP_ID"
Set them:
gh variable set FIAB_TENANT_ADMIN_OID # prompts; paste your user object id
gh variable set FIAB_ADMIN_GROUP_ID # prompts; paste the group object id
Confirm they took. Repository variables are not secrets, so gh variable list prints their values — verify by name and by the value you set:
Confirm the deploy resolved them. This is the check that matters, because it reads what was actually composed rather than what you intended. Every deploy run emits a notice:
Bootstrap admin binding: group=present, oid=present (source=FIAB_TENANT_ADMIN_OID-repo-var). Values redacted.
group=absent, oid=absent means the run will refuse rather than deploy an unusable Console. That refusal is correct behaviour, not a bug.
Part 2 — credential reference, per cloud¶
Names only. Nothing on this page ever prints a value.
| Name | Kind | Required | Purpose |
|---|---|---|---|
AZURE_CLIENT_ID | secret | yes | Deploy SP application (client) id |
AZURE_CLIENT_SECRET | secret | yes | Deploy SP secret |
AZURE_TENANT_ID | secret | yes | Entra tenant |
AZURE_SUBSCRIPTION_ID | secret | yes | Target subscription |
FIAB_TENANT_ADMIN_OID | variable | strongly recommended | Bootstrap admin user object id |
FIAB_ADMIN_GROUP_ID | secret or variable | one of these two | Admin security-group object id |
Workflow: deploy-fiab-commercial.yml · app tier: full-app-deploy-commercial.yml
| Name | Kind | Required | Purpose |
|---|---|---|---|
AZURE_GCC_CLIENT_ID | secret | yes | Deploy SP in the GCC tenant |
AZURE_GCC_CLIENT_SECRET | secret | yes | |
AZURE_GCC_TENANT_ID | secret | yes | |
AZURE_GCC_SUBSCRIPTION_ID | secret | yes | |
FIAB_GCC_ADMIN_GROUP_ID | secret | yes | Admin group in the GCC tenant |
GCC is Azure Public ARM endpoints under M365 GCC identity — the SP is created in the GCC tenant, not the Commercial one. Workflow: deploy-fiab-gcc.yml
GCC is infrastructure-only today (#3078)
gcc.bicepparam deliberately never sets deployAppsEnabled, and main.bicep defaults it to false. A full GCC run therefore provisions infrastructure and creates zero Container Apps — no loom-console. A green run on this lane does not mean GCC serves Loom. The gap is a missing GCC image producer, tracked with an owner in #3078. Disclosed here rather than implied working, per cloud-parity.md.
| Name | Kind | Required | Purpose |
|---|---|---|---|
AZURE_GOV_CLIENT_ID | secret | yes | Deploy SP in the Azure Government tenant |
AZURE_GOV_CLIENT_SECRET | secret | yes | |
AZURE_GOV_TENANT_ID | secret | yes | |
AZURE_GOV_SUBSCRIPTION_ID | secret | yes | |
FIAB_GOV_ADMIN_GROUP_ID | secret | yes | Admin group in the Gov tenant |
LOOM_GOV_MSAL_CLIENT_ID | secret | optional | Pins an existing sign-in app registration instead of creating one |
LOOM_GOV_MSAL_CLIENT_SECRET | secret | optional | Paired with the above |
The SP must be created against the Azure Government endpoints:
Workflow: deploy-fiab-gcch.yml (environment-approval gated) · images: gov-build-images.yml
Same AZURE_GOV_* + FIAB_GOV_ADMIN_GROUP_ID set as GCC-High — IL5 uses the Azure Government endpoints and the same secret names.
Workflow: deploy-fiab-il5.yml · param file: il5.bicepparam (deployAppsEnabled = true)
IL5 has never been executed
Measured 2026-08-13: gh run list --workflow deploy-fiab-il5.yml returns nothing. The lane is wired and its parameter file enables the app tier, but no run has ever produced a receipt. Treat every IL5 statement on this page as unverified, and expect to be the first to exercise it. A workflow that has never run is the loudest signal available, not a quiet pass.
Verify the whole set at once:
gh secret list # names + timestamps only — values are never shown
gh variable list # names + values (variables are not secrets)
Part 3 — deploy: greenfield¶
Greenfield means the target subscription holds no Azure resource Loom would adopt and no existing rg-csa-loom-admin-* hub. Every backing service is created new.
Not sure? Run the read-only inventory — if it returns no candidates in any subscription you intend to use, you are greenfield:
If it returns candidates, stop and use Part 4 — brownfield instead. Greenfield working proves nothing about brownfield, and the two are verified independently.
Why this takes three phases¶
This is the single most common "is this a bug?" question, so it is answered before the steps rather than after.
A fresh deploy creates an empty Azure Container Registry. The Console and its sibling Container Apps reference <newacr>.azurecr.io/loom-console:<tag>. With deployAppsEnabled=true against a brand-new registry, ARM tries to create those apps before any image exists and every Container App PUT fails with a manifest/pull error.
That is expected, not a defect. The image build is a required phase, and no single one-shot deploy can collapse it, because the registry that must hold the images is created by the same deployment that needs to pull from them. Phase 1 therefore overrides the parameter file with deployAppsEnabled=false.
-
Phase 1 — infrastructure (40–90 min)
Hub VNet, Private DNS, ACR, Container Apps Environment, Key Vault, and every Azure backing service. Creates no Container Apps.
-
Phase 2 — images and apps (15–25 min)
Opens the private ACR, builds every image server-side with
az acr build, re-locks the registry, brings the Container Apps up on the images it pushed. -
Phase 3 — post-deploy bootstrap (10–15 min)
The grants Bicep cannot make: the MSAL app registration, Synapse SQL admin, Purview roles, Databricks SCIM, the Spark private-endpoint fix. Sign-in does not work until this runs.
Step 0 — verify the credentials before you spend anything¶
Dispatch a whatif-only run. It authenticates, validates the template against your real tenant, and provisions nothing. This is the cheapest possible proof that P1, P2 and P3 all landed.
region is required and has no default
The region is the identity of the estate — rg-csa-loom-admin-<region>, vnet-csa-loom-hub-<region> and uami-loom-console-<region> all derive from it. A wrong region does not fail; it succeeds against a different, empty estate. GitHub rejects a dispatch that omits it.
Confirm it worked:
RUN_ID=$(gh run list --workflow deploy-fiab-commercial.yml --limit 1 \
--json databaseId --jq '.[0].databaseId')
gh run watch "$RUN_ID" --exit-status
Expect a green run whose title carries — DRY RUN (whatif-only, applies nothing). That marker is deliberate: it tells the deploy-staleness watchdog this run changed nothing, so a dry run can never silence a real deploy gap.
Phase 1 — infrastructure¶
Preview first. This is free and it catches CIDR, quota and SKU problems before anything is created:
git clone https://github.com/fgarofalo56/csa-inabox.git
cd csa-inabox
GROUP_ID=$(az ad group show --group "Loom Admins" --query id -o tsv)
az deployment sub create \
--location <region> \
--template-file platform/fiab/bicep/main.bicep \
--parameters platform/fiab/bicep/params/commercial-full.bicepparam \
--parameters adminEntraGroupId="$GROUP_ID" \
--parameters deployAppsEnabled=false \
--what-if
Then apply:
az deployment sub create \
--name "csa-loom-phase1-$(date +%Y%m%d-%H%M)" \
--location <region> \
--template-file platform/fiab/bicep/main.bicep \
--parameters platform/fiab/bicep/params/commercial-full.bicepparam \
--parameters adminEntraGroupId="$GROUP_ID" \
--parameters deployAppsEnabled=false
What this changes. Creates rg-csa-loom-admin-<region> and the DLZ resource groups, the hub VNet and Private DNS zones, the ACR, the Container Apps Environment, Key Vault, and every Azure backend. It creates no Container Apps.
How to confirm it worked:
# The admin resource group exists and provisioning succeeded.
az group show --name "rg-csa-loom-admin-<region>" --query properties.provisioningState -o tsv
# Expect: Succeeded
# The ACR exists and is EMPTY — an empty repository list here is the CORRECT
# phase-1 outcome, not a failure.
ACR=$(az acr list --resource-group "rg-csa-loom-admin-<region>" --query "[0].name" -o tsv)
az acr repository list --name "$ACR" -o tsv | wc -l # expect 0
# No Container Apps yet — also correct.
az containerapp list --resource-group "rg-csa-loom-admin-<region>" -o tsv | wc -l # expect 0
Resource-provider registration is not automatic on the by-hand path
Running az deployment sub create yourself does not register missing resource providers. lib/setup/deploy-preflight.ts reads registration state and emits the az provider register --namespace <ns> lines for any that are missing; automatic registration only happens on the CI deploy path, where scripts/ci/deploy-retry.mjs --remediate reads the namespace out of a MissingSubscriptionRegistration failure, registers it, and retries once. Either register up front, or dispatch the workflow instead of running the CLI by hand.
Phase 2 — build the images and bring the apps up¶
Leaving region empty is supported here — the resolve job asks Azure Resource Graph which rg-csa-loom-admin-* exists and targets that. Supply it only when the subscription holds more than one admin plane.
What this changes. Temporarily opens the ACR firewall, runs az acr build server-side for every app image, re-locks the registry, then re-runs the deploy with deployAppsEnabled=true so the Container Apps are created on the images it just pushed.
How to confirm it worked:
az acr repository list --name "$ACR" -o tsv # now non-empty
az containerapp list --resource-group "rg-csa-loom-admin-<region>" \
--query "[].{name:name,fqdn:properties.configuration.ingress.fqdn}" -o table
# The Console answers, and reports the commit it is running.
curl -s https://<your-console-hostname>/build-marker.txt
Phase 3 — post-deploy bootstrap¶
gh workflow run csa-loom-post-deploy-bootstrap.yml \
-f boundary=Commercial \
-f region=<region> \
-f admin_subscription=<subscription-id>
Leave dlz_subscription and dlz_domain empty — both are discovered from Resource Graph across every subscription the deploy identity can read. Supply them only to disambiguate an estate with several landing zones.
What this changes. Creates the MSAL app registration and writes its secret to Key Vault, wires LOOM_MSAL_CLIENT_ID onto the Console, grants the Console identity Synapse Administrator, registers it with Databricks via SCIM, assigns Microsoft Graph application roles, and creates the AI Search governance index.
How to confirm it worked: open the Console in a browser and sign in. Sign-in working is the verification — it exercises the app registration, the Key Vault secret reference, and the redirect URI in one action. Then open /admin/readiness and confirm your account can reach it; if /admin/* 403s, revisit P3.
Part 4 — deploy: brownfield¶
Brownfield means your tenant already holds something Loom could use: a Purview account, a shared AI Search, an ADLS lake, an existing VNet, or a previous Loom hub. This is a separate, complete path — not a footnote on greenfield. Silently deploying a second Purview beside yours is a violation; so is failing because one exists.
The phase shape is identical to greenfield (infrastructure → images → bootstrap). What differs is that a discovery and decision step comes first, and the values you choose are supplied to phase 1.
Step 1 — the multi-subscription analysis¶
Read-only. It spans every subscription the identity can read and reports what exists, what Loom would use each one for, and what it would change about it.
What this changes. Nothing — it only reads. It emits ready-to-source EXISTING_* exports for each reusable resource.
How to confirm it worked: it prints one candidate block per discovered service. If it prints none, you are greenfield after all — go back to Part 3. Full detail of what it scans and the permissions it needs is in Discovery and adoption.
Step 2 — decide adopt / create / skip, per service¶
Three decisions per service, never assumed:
- adopt — use the existing resource. Loom binds to it and suppresses creating its own.
- create — deploy a new one alongside, deliberately.
- skip — do not provision at all; the dependent surfaces honest-gate.
The complete per-service table — which EXISTING_* variables each reads, which provision* flag each suppresses, and the caveats — is in Brownfield → step 2.
Drive brownfield from the CLI, not the wizard (#3342)
The in-Console setup wizard gates its Deploy button on planBlockers(), which blocks every adopt decision that carries no fitness verdict — and no production code path attaches one. Because recommendFor() picks adopt by default whenever a candidate is found, that is the default outcome on a brownfield tenant, not an edge case. Use the CLI path below. Tracked in #3342.
Step 3 — supply the values¶
Every boundary parameter file reads readEnvironmentVariable('EXISTING_*', ''), so exporting the variables before the deploy is the supported input path — not an undocumented override.
export EXISTING_PURVIEW=<purview-account-name>
export EXISTING_PURVIEW_RG=<resource-group>
export EXISTING_PURVIEW_SUB=<subscription-id> # only when cross-subscription
export EXISTING_AI_SEARCH_SERVICE=<search-service-name>
export EXISTING_AI_SEARCH_RG=<resource-group>
# ...then phase 1 exactly as in greenfield, with deployAppsEnabled=false.
Some EXISTING_* names are accepted but have no consumer
EXISTING_STORAGE, EXISTING_POSTGRES, EXISTING_KEYVAULT, EXISTING_FIREWALL are accepted by the discovery tooling but no Bicep parameter reads them — setting them has no effect at deploy time. Named here rather than left for you to discover by watching a second lake get created. The authoritative, re-measurable list is in Brownfield → step 3.
Step 4 — deploy into an existing Loom hub¶
If the subscription already holds a Loom hub, the topology guard rejects a tenant deploy so a second Console can never be stamped. To reconcile the existing hub in place:
gh workflow run deploy-fiab-commercial.yml \
-f run_mode=full \
-f region=<region> \
-f allow_existing_hub=true
What this changes. Reconciles the existing admin resource group incrementally and re-renders every LOOM_* env var on the Container Apps. It does not stamp a second Console, and it does not tear anything down.
Step 5 — what is validated, and what happens when it fails¶
Each adopted resource is validated for SKU, region, network reachability, and whether the deploy identity holds (or can be granted) the RBAC it needs. When validation fails you get the specific reason and the exact remediation, not a generic failure. The per-service validation matrix and the failure behaviour are in Brownfield → step 4 and step 5.
Part 5 — verify the deploy¶
Run these in order. Each answers a different question, and a later one passing does not imply an earlier one did.
| # | Check | Command | Pass looks like |
|---|---|---|---|
| 1 | The infrastructure exists | az group show -n rg-csa-loom-admin-<region> --query properties.provisioningState -o tsv | Succeeded |
| 2 | The images exist | az acr repository list --name <acr> -o tsv | a non-empty list |
| 3 | The apps are running | az containerapp list -g rg-csa-loom-admin-<region> --query "[].properties.runningStatus" -o tsv | Running per app |
| 4 | The Console answers | curl -s https://<console-host>/build-marker.txt | a commit SHA |
| 5 | The estate is not behind main | git log --oneline <that-sha>..origin/main \| wc -l | 0 |
| 6 | Sign-in works | open the Console in a browser | you reach the home page |
| 7 | You are an admin | open /admin/readiness | it renders, not 403 |
Check 5 is the one people skip, and it is the one that bites
A merge is not a deploy. If check 5 returns a non-zero count, every fix merged in that window is inert on your estate — present in the repo, absent from the thing you are looking at. Report that state in exactly those words: merged, not deployed. This is deploy-integrity.md R2 and R3, and it exists because a live estate once sat eight merges behind main for two weeks while work continued around it.
Part 6 — when a step fails¶
Failures are classified rather than dumped. The eight classes — transient, eventual-consistency, registration, permission, quota, config, defect, unknown — and the ARM codes that map to each are in Failure recovery. The four you are most likely to meet on a first deploy:
| Symptom | Class | What to do |
|---|---|---|
MissingSubscriptionRegistration | registration | az provider register --namespace <ns>; re-run. The CI path does this automatically and retries once. |
MANIFEST_UNKNOWN / image pull failure on a Container App | config | You ran phase 1 with deployAppsEnabled=true on an empty ACR. Re-run phase 1 with deployAppsEnabled=false, then phase 2. |
AuthorizationFailed writing a role assignment | permission | The deploy SP is missing User Access Administrator. See P2. |
RoleAssignmentExists on a re-deploy | transient/config | Dispatch with -f skip_role_grants=true. |
| The run refuses with "No bootstrap tenant-admin binding could be resolved" | config | Correct behaviour, not a bug. See P3. |
| The run refuses on a region mismatch | config | The region you passed does not match the hub in the subscription. Pass the hub's actual region. |
Two defaults worth knowing before you dispatch anything
keep_resourcesdefaultstrue— afullrun reconciles; it does not destroy. Teardown additionally requiresconfirm_teardown_rgto equal the resolved admin resource group exactly. Assuming the opposite is dangerous in the other direction, so it is stated plainly here.deploy_apps_enableddefaultstrue— afullrun creates/updates the Container Apps and re-renders everyLOOM_*env var on them. Set itfalseonly for phase 1 of a from-scratch install, where the ACR is still empty.
Per-cloud status — measured, not claimed¶
Re-measured 2026-08-13 with gh run list. Every row is command output. cloud-parity.md forbids implying parity that has not been verified, so an unverified lane says so.
| Lane | Last 3 runs | What that proves |
|---|---|---|
deploy-fiab-commercial | in-progress · failure · success (all 2026-08-13) | Actively exercised. Mixed, so check the specific run you care about. |
full-app-deploy-commercial | cancelled (08-13) · success (08-08) · failure (08-08) | The app tier has succeeded recently. A from-scratch phase 1→2→3 into a genuinely empty subscription has not been performed for this revision. |
deploy-fiab-gcc | success · success · success | Green — and deploys zero Container Apps (#3078). Green here does not mean GCC serves Loom. |
deploy-fiab-gcch | failure (08-13) · failure (08-12) · success (08-11) | Currently red. Read the run before trusting any GCC-High statement. |
deploy-fiab-il5 | never run | Entirely unverified. |
gov-build-images | success · success (both 08-08) | The Gov image producer works standalone; unexercised as part of an end-to-end Gov install. |
Re-measure it yourself rather than trusting a published number — these drift with every merge:
for wf in deploy-fiab-commercial full-app-deploy-commercial deploy-fiab-gcc \
deploy-fiab-gcch deploy-fiab-il5 gov-build-images \
csa-loom-post-deploy-bootstrap; do
echo "== $wf"
gh run list --workflow "$wf.yml" --limit 3 \
--json conclusion,createdAt --jq '.[] | "\(.conclusion // "in-progress") \(.createdAt[0:10])"'
done
A workflow that prints nothing has never run.
Post-deploy tenant consents (optional, feature-scoped)¶
None of these block the deploy or sign-in. Each unlocks a specific surface, and until it is done that surface shows an honest gate rather than a broken screen. They are one-time and idempotent.
| Consent | Who | Unlocks |
|---|---|---|
Grant the deploy SP AppRoleAssignment.ReadWrite.All on Microsoft Graph | Global Administrator or Privileged Role Administrator | /admin/security MIP + DLP tabs, the Identity Picker |
Admin-consent Azure Service Management / user_impersonation on the MSAL app registration | Global Administrator or Application Administrator | Connections cross-subscription discovery |
| Tenant-root management group Reader for the deploy SP | Owner / UAA at the root MG | Connections cross-subscription enumeration |
| Add the Console identity as Power Platform Administrator | Power Platform admin centre | Power Platform + Copilot Studio surfaces |
| Enable the Power BI tenant SP setting | Fabric admin portal | The opt-in Power BI backend only — not required by default |
The full, copy-pasteable command for each is in v3 tenant bootstrap → day-one operator prerequisites. They are reproduced there rather than duplicated here so there is exactly one copy to keep correct.
Assigning a Graph app role to a managed identity is the grant
There is no separate "Grant admin consent" click for the Console identity — that is the app-registration pattern, not the managed-identity pattern. The only reason a human is involved at all is that the deploy SP needs AppRoleAssignment.ReadWrite.All before it can make those assignments unattended. Once granted, every subsequent bootstrap run does it for you.
Where this page sits¶
This page is the spine: prerequisites → deploy → verify. The pages below are depth, and each is linked from the relevant step above rather than competing with it.
-
Every phase in detail, per-boundary command variants, and the region caveats.
-
The complete adopt/create matrix, validation per service, and the open gaps.
-
What the scan reads, what Loom changes about an adopted service.
-
The eight failure classes and the remediation for each.
-
Resource groups, naming and tags
The naming contract and the teardown blast radius.
-
The one-time tenant actions, in full.
Found a prerequisite that should not exist?¶
That is the point of this page having a count at the top. If a step here asks you to set a value, grant a role, or run a script that the deploy could have done itself, it is a defect under auto-bind-by-default.md §5 — not a chore.
File it with the label csa-loom, quoting the step and what you think the deploy should have produced.