Implement CI/CD for Foundry hosted agents
Customer scenario
Fabrikam uses three AI agents to review code changes:
- The scanner identifies security vulnerabilities.
- The reviewer assesses maintainability and release risk.
- The orchestrator invokes both specialists and produces one release recommendation.
Scanner, reviewer, and orchestrator are Microsoft Foundry hosted agents v2. Each successful azd deploy creates a new immutable Foundry agent version. Calls use the hosted-agent Responses endpoint only after creating a session with a concrete version_ref, so the session is bound to the recorded immutable version. They aren't Azure Container Apps.
Fabrikam also operates one deliberately thin traditional web application: a Container Apps release dashboard and gateway. Each dashboard revision targets one exact orchestrator version and displays:
- Its Container App revision and release channel.
- The selected Foundry release-set ID.
- The immutable orchestrator agent name, Foundry version, and Responses endpoint.
The dashboard is the progressive-delivery boundary. During a canary, Container Apps sends 75 percent of requests to the stable dashboard revision and 25 percent to the candidate dashboard revision. The agents don't claim native weighted Foundry routing.
Lab scenario
Prepare passwordless GitHub access to Azure, activate the supplied workflows, and deploy the initial release to a development environment. Then change the scanner's logical version, review credential-free pull-request evidence, and deploy a candidate release set.
Use the dashboard's weighted URL to observe 75/25 routing. Use the stable and candidate label URLs for deterministic verification. Promote healthy dashboard traffic to 0/100, then exercise a regression profile. Rollback must use a persisted last-verified release manifest, restore the dashboard revision that targets that compatible Foundry release set, and verify the exact restored orchestrator version both through the dashboard and by version-pinned invocation.
By the end of this exercise, you'll be able to:
- Validate strict logical semantic versions, tool contracts, a dependency DAG, model policy, and hosted-agent v2 manifests without Azure credentials.
- Deploy Foundry hosted agents serially in shared azd state and capture immutable names, versions, and Responses endpoints.
- Bind deterministic smoke evaluation evidence to a source commit and Foundry release set.
- Use GitHub environments, OIDC, Bicep, and separate Foundry projects for development, staging, and production.
- Apply canary and blue-green traffic to a traditional Container Apps boundary without misrepresenting Foundry routing.
- Restore a compatible release set from persisted evidence and retain reverse-dependency rollback closure.
[!IMPORTANT] This lab creates billable Azure Container Registry, Log Analytics, Container Apps, and Microsoft Foundry resources, including a
gpt-5.4-minimodel deployment pinned to version2026-03-17. Use an isolated training subscription and remove the resource groups when you finish. Model availability and quota vary by region.
Understand the three version identities
The lab intentionally keeps three version systems separate.
| Identity | Example | Purpose |
|---|---|---|
| Logical semantic version | scanner 1.2.0 |
Source compatibility and dependency ranges |
| Immutable Foundry agent version | fabrikam-code-scanner, version 7 |
Deployed executable agent, version endpoint, and version-bound session |
| Dashboard Container App revision | fabrikam-release-dev--abc123 |
Traditional-app release channel and 75/25 or 0/100 traffic |
A logical version isn't a Foundry version. A Foundry version isn't a Container App revision. The release-set manifest records all three and prevents the pipeline from treating them as interchangeable.
Understand the release architecture
The deployment graph is:
scanner --------+
+--> orchestrator --> version-bound hosted-agent session
reviewer --------+ ^
|
dashboard revision
(stable or candidate)
The source has four parts:
azure.yamlandsrc/*_agent.pydefine the three hosted agents;dashboard/app.pyis the only Container Apps service.agents/*.ymldefines logical versions, dependencies, model policy, evaluation identity, instructions, and tool contracts.scripts/captures immutable deployment outputs into a release set, whileassets/evaluation/and the quality profiles provide validation evidence.infra/andassets/workflows/create the Azure boundary and provide the inactive workflow templates that you activate during the lab.
The captured release set binds the reviewed source and policy metadata to each immutable Foundry agent version and the dashboard revision that invokes the orchestrator.
Task 1: Prepare the lab
You need:
- An Azure subscription in which an administrator can grant the required roles.
- A GitHub account that can create a private repository, workflows, environments, variables, protection rules, releases, and issues.
- Python 3.13, Git, Azure CLI, and Azure Developer CLI.
- Visual Studio Code with the Python, GitHub Actions, and Bicep extensions.
- Clone the lab source repository, open it in Visual Studio Code, and change to the starter directory:
git clone https://github.com/MicrosoftLearning/mslearn-ai-multi-agents.git
cd mslearn-ai-multi-agents\Allfiles\09-fabrikam-multi-agent-cicd
- Sign in and confirm the subscription:
az login
az account show --output table
$env:AZURE_DEV_USER_AGENT = 'microsoft_foundry_skill'; azd auth login
-
Use synthetic data only. Don't add an Azure client secret or an
AZURE_CREDENTIALSsecret to GitHub. -
Open
azure.yaml. Confirm that scanner, reviewer, and orchestrator are hosted agents and dashboard is the only Container Apps service. -
Open
agents/orchestrator.yml. Identify its logical version, scanner/reviewer dependency ranges, exact model version, evaluation identity, and hosted Responses manifest. -
Open
dashboard/app.py. Find the release-set, release-channel, Container App revision, orchestrator version, and Responses endpoint fields shown to learners.
Task 2: Validate the starter locally
- Create the virtual environment and install dependencies:
.\scripts\setup.ps1
. .\.venv\Scripts\Activate.ps1
- Run the complete credential-free validation:
python scripts\preflight.py
python scripts\validate_workflows.py
python -m unittest discover -s Allfiles\09-fabrikam-multi-agent-cicd\tests -v
python scripts\export_release.py --agents agents --manifest artifacts\release-set.json --contracts artifacts\current-contracts.json --source-commit local-validation --environment pr-validation --status validated
python -m src.main validate --manifest artifacts\release-set.json --baseline assets\contracts\baseline.json --candidate artifacts\current-contracts.json --quality assets\quality-metrics-healthy.json --output artifacts\compatibility-report.json
az bicep build --file infra\main.bicep
Expected results:
- Preflight reports three Foundry hosted agents v2 and one dashboard.
- Workflow validation confirms OIDC, serial deployments, version-pinned smoke calls, persisted rollback evidence, and dashboard-only traffic.
- Targeted tests pass.
- The compatibility report has
"compatible": true. - Bicep builds without errors.
-
Open
artifacts/release-set.json. Confirm that remote Foundry names, versions, and endpoints arenull. Pull-request validation is credential-free and can't invent deployment evidence. The deployment workflow fills those fields only after successfulazd deploy. -
Optional: press F5 and choose a hosted-agent configuration to start the selected entrypoint with the VS Code Python debugger. Foundry Toolkit Agent Inspector can connect to the local hosted-agent server after you supply the required local environment values.
Task 3: Create the practice repository
- On GitHub, create a private repository named
fabrikam-agent-cicd. - Don't initialize it with a README,
.gitignore, or license. - Record the exact
<owner>/fabrikam-agent-cicdvalue. - Copy the starter into a separate practice repository:
$labRoot = (Get-Location).Path
$practiceRepo = Join-Path (Split-Path $labRoot -Parent) 'fabrikam-agent-cicd'
New-Item -ItemType Directory -Force $practiceRepo
Get-ChildItem $labRoot -Force |
Where-Object { $_.Name -notin @('.azure', '.env', '.venv', 'artifacts', '__pycache__') } |
Copy-Item -Destination $practiceRepo -Recurse -Force
Set-Location $practiceRepo
git init
git branch -M main
git remote add origin '<repository-url>'
- Don't push yet. A push to
maindeploys development.
Task 4: Configure passwordless GitHub access
GitHub Actions exchanges a short-lived GitHub OIDC token for an Entra token. Don't create a client secret.
- In the Microsoft Entra admin center, create a single-tenant app registration named
fabrikam-agent-github. - Record:
- Application (client) ID as
AZURE_CLIENT_ID. - Directory (tenant) ID as
AZURE_TENANT_ID.
- Application (client) ID as
- Open the linked Enterprise application and record its Object ID as
GITHUB_PRINCIPAL_ID. - Add one federated credential for each GitHub environment:
repo:<owner>/fabrikam-agent-cicd:environment:development
repo:<owner>/fabrikam-agent-cicd:environment:staging
repo:<owner>/fabrikam-agent-cicd:environment:production
- Select or create one resource group per environment:
$azureRegion = 'eastus2'
$suffix = (New-Guid).Guid.Substring(0, 8)
@('dev', 'stg', 'prod') | ForEach-Object {
az group create --name "rg-lab09-$($_)-$suffix" --location $azureRegion | Out-Null
}
- An Azure administrator must assign the Enterprise application these roles on each selected resource group:
- Contributor
- Role Based Access Control Administrator
These permissions allow the workflow to create environment-specific Foundry projects, model deployments, identities, RBAC assignments, and dashboard resources.
Task 5: Configure GitHub environments
- In repository Settings > Environments, create:
developmentstagingproduction
- Add these environment variables to each environment:
| Variable | Development example | Requirement |
|---|---|---|
AZURE_CLIENT_ID |
<application-client-id> |
Same OIDC application |
AZURE_TENANT_ID |
<directory-tenant-id> |
Same tenant |
AZURE_SUBSCRIPTION_ID |
<subscription-id> |
Target subscription |
AZURE_LOCATION |
eastus2 |
Region with exact model quota |
AZURE_ENV_NAME |
lab09-cicd-dev |
Unique per environment |
GITHUB_PRINCIPAL_ID |
<enterprise-app-object-id> |
Enterprise application Object ID |
AZURE_RESOURCE_GROUP_NAME |
rg-lab09-dev-... |
Unique per environment |
- Use
lab09-cicd-stgandlab09-cicd-prodplus their distinct resource groups for staging and production. - Add required reviewers and restrict deployment branches for staging and production.
Each GitHub environment maps to a separate resource group and Foundry project. The workflow promotes the same reviewed source identity. Production additionally verifies that staging has a persisted verified manifest for the same commit and reviewed release ID.
Task 6: Activate the workflows
- Create
.github/workflows:
New-Item -ItemType Directory -Force .github\workflows
- Copy the four supplied templates:
Copy-Item assets\workflows\validate-agents.yml .github\workflows\validate-agents.yml
Copy-Item assets\workflows\deploy-environment.yml .github\workflows\deploy-environment.yml
Copy-Item assets\workflows\canary-quality-gate.yml .github\workflows\canary-quality-gate.yml
Copy-Item assets\workflows\rollback-agents.yml .github\workflows\rollback-agents.yml
- Review
.github/workflows/validate-agents.yml.
It has no Azure login. It validates:
- Contracts and strict logical semantic versions.
- Dependency DAG and reverse-dependency closure.
- Exact
gpt-5.4-mini/2026-03-17model policy. - Hosted-agent v2 declarations and separate entrypoints.
- Source compilation, targeted tests, workflows, and deterministic dataset shape.
- Review
.github/workflows/deploy-environment.yml.
The important deployment order is:
azd deploy scanner
azd ai agent show scanner
azd ai agent invoke scanner --version <captured-version>
azd deploy reviewer
azd ai agent show reviewer
azd ai agent invoke reviewer --version <captured-version>
azd deploy orchestrator
azd ai agent show orchestrator
azd ai agent invoke orchestrator --version <captured-version>
azd deploy dashboard
Every azd command sets AZURE_DEV_USER_AGENT=microsoft_foundry_skill inline. Scanner and reviewer are serial because concurrent deploys must not mutate the same selected azd state. Orchestrator deploys only after their immutable endpoints exist. Deployment evidence also records a concrete /versions/<version> endpoint for CLI verification. Runtime HTTP calls create a session with that same concrete version and reject a mismatched returned version.
- Find
capture_release_set.pyin the deployment workflow. It captures:
AGENT_SCANNER_NAME
AGENT_SCANNER_VERSION
AGENT_SCANNER_RESPONSES_ENDPOINT
AGENT_REVIEWER_NAME
AGENT_REVIEWER_VERSION
AGENT_REVIEWER_RESPONSES_ENDPOINT
AGENT_ORCHESTRATOR_NAME
AGENT_ORCHESTRATOR_VERSION
AGENT_ORCHESTRATOR_RESPONSES_ENDPOINT
- Review
.github/workflows/canary-quality-gate.yml.
It refuses promotion unless the candidate artifact contains passing version-bound evaluation evidence. The healthy/regression fixture is secondary evidence used only to exercise the policy branches.
- Review
.github/workflows/rollback-agents.yml.
It downloads release-set.json from the persistent verified-<environment> GitHub release, validates environment, verified status, release-set identity, all agent bindings, dashboard app/revision, and stable label evidence, then restores it. It doesn't infer that the second-newest revision is safe.
The deploy, quality, and rollback workflows share one environment-keyed concurrency group with cancel-in-progress: false, so two operations can't race while changing dashboard traffic or persisted release evidence.
- Validate the activated copies:
python scripts\validate_workflows.py
git status --short
Task 7: Deploy the initial development release
- Commit and push:
git add .
git commit -m "Initialize Foundry hosted-agent delivery"
git push -u origin main
- In GitHub Actions, open Deploy hosted-agent release set.
Observe:
- Credential-free validation completes first.
- GitHub exchanges its environment OIDC token; no stored client secret is used.
- Bicep creates a development Foundry project, exact model deployment, ACR, Log Analytics, Container Apps environment, and one dashboard Container App.
- Scanner, reviewer, and orchestrator each create an immutable Foundry hosted-agent version.
- Each agent is checked with
azd ai agent showand a version-pinned smoke invocation. - The orchestrator's environment points to captured scanner and reviewer Responses endpoints plus exact versions; each call creates and verifies a session pinned to that version.
- The two-record deterministic dataset runs against the pinned orchestrator.
- One dashboard revision is deployed with 100 percent traffic and displays the exact selected release set.
- Because no
verified-developmentrelease exists and Azure reports only the infrastructure bootstrap revision, the workflow treats this as an explicit first release. After smoke, evaluation, stable-label dashboard, and version-evidence checks pass, it persists this release as the first verified baseline.
- Download
deployment-evidence-development-<run-id>. - Open
release-set.json. Compare each logical version with the immutable Foundry version. - Open
evaluation-evidence.json. Confirm the source commit, release-set ID, exact orchestrator version, dataset/evaluator identity, and"status": "passed".
Task 8: Verify the initial resources
- In the Azure portal, open the development resource group.
- Open the Microsoft Foundry project.
- Confirm the
gpt-5.4-minideployment is version2026-03-17. - Open Agents and confirm scanner, reviewer, and orchestrator are hosted agents with active immutable versions.
- Compare the agent names and versions with
release-set.json. - Confirm there aren't scanner, reviewer, or orchestrator Container Apps.
- Open the single Fabrikam release dashboard Container App.
- Under Revision management, confirm one selected dashboard revision has 100 percent traffic.
- Open the dashboard URL. Confirm it displays:
STABLEchannel.- Its Container App revision.
- Release-set ID.
- Exact orchestrator name, Foundry version, and Responses endpoint.
- Submit the synthetic review form. Confirm the dashboard invokes the selected orchestrator.
Task 9: Create and review a candidate
- Create a branch:
git checkout -b feature/scanner-release-metadata
- In
agents/scanner.yml, change:
version: 1.2.0
to:
version: 1.2.1
- Add one instruction sentence that doesn't change the tool contract:
Include a short confidence explanation for each reported finding.
- Run local validation.
- Commit, push, and create a pull request:
git add agents\scanner.yml
git commit -m "Clarify scanner finding confidence"
git push -u origin feature/scanner-release-metadata
-
Open the pull-request workflow artifact.
-
Confirm:
- Scanner logical version is
1.2.1. - Contract digest is present.
- No breaking contract finding exists.
- Orchestrator's
>=1.0.0,<2.0.0dependency accepts scanner1.2.1. - Hosted manifests remain Responses
2.0.0, Python3.13. - Remote Foundry deployment fields remain
null.
- Scanner logical version is
-
Merge the pull request.
Task 10: Verify 75/25 dashboard exposure
After the merge deployment succeeds:
- Download its deployment evidence.
- Confirm the candidate release set has new immutable Foundry versions and a new release-set ID.
- Confirm its dashboard revision records the candidate orchestrator's exact version endpoint and uses a Responses session pinned to that version.
- Open the dashboard's normal URL and refresh at least 12 times.
- Record when the page shows:
- Stable channel and the last-verified release-set identity.
- Candidate channel and the candidate release-set identity.
The sample is small, so don't expect exactly nine stable and three candidate responses. The configured weights, not a short random sample, are authoritative.
- Open the
stable_label_urlfrom the candidaterelease-set.json. Its hostname beginsstable---; the workflow obtains the app FQDN from Azure and verifies the label-to-revision mapping before recording it. - Refresh it three times. Confirm it always reports the same stable dashboard revision and orchestrator version.
- Open the
candidate_label_url. - Refresh it three times. Confirm it always reports the candidate dashboard revision and candidate orchestrator version.
- In the Azure portal, open dashboard Revision management and verify the 75/25 weights and
stable/candidatelabels.
These direct label routes are deterministic verification. Learners must not rely only on random refreshes.
Task 11: Promote the candidate across environments
-
In GitHub Actions, run Assess dashboard canary quality with:
- Environment:
development - Deployment run ID: candidate deployment run ID
- Failed agent:
scanner - Metric profile:
healthy
- Environment:
-
Confirm:
- Version-bound evaluation evidence is passing.
- Policy action is
promote. - Dashboard traffic changes from 75/25 to 0/100.
- Candidate dashboard revision receives the
stablelabel. verified-developmentcontains the persisted verifiedrelease-set.json.
-
Open the stable label URL and verify the promoted release-set and exact orchestrator version.
-
Run Deploy hosted-agent release set manually for
staging. -
Enter the same reviewed source commit deployed to development.
-
Approve the protected staging environment.
-
Run its healthy canary assessment to create
verified-staging. -
Run Deploy hosted-agent release set for
productionwith the same source commit. -
Confirm production waits for its protected-environment approval and verifies staging's:
- Source commit.
- Reviewed release ID.
- Verified status.
The Foundry agent version numbers can differ across projects. The reviewed source identity, logical versions, contracts, model policy, and evaluation identity remain the same.
Task 12: Exercise compatible-set rollback
-
Run Assess dashboard canary quality for a candidate with:
- Metric profile:
regression - Failed agent:
scanner
- Metric profile:
-
Confirm policy action is
rollback. -
Open Restore verified Foundry release set.
-
Confirm it:
- Downloads the candidate release set from the deployment run.
- Downloads the persisted last-verified environment manifest.
- Derives rollback closure
orchestrator, scannerin reverse dependency order. - Restores 100 percent dashboard traffic to the exact revision recorded in the verified manifest.
- Applies the
stablelabel to that revision. - Verifies the dashboard reports the persisted release-set ID and exact orchestrator version.
- Invokes that exact Foundry orchestrator version with
azd ai agent invoke --version. - Creates a GitHub incident issue and uploads rollback evidence.
-
Open
rollback-evidence.json. Confirm it records:- Restored release-set ID.
- Dashboard revision.
- Exact orchestrator name, version, and Responses endpoint.
- 100 percent traffic.
-
Open the stable dashboard URL and verify the same values.
Rollback doesn't delete immutable Foundry versions. It restores traffic to a dashboard revision that is already bound to a known-compatible Foundry release set.
Task 13: Review the release evidence
Retain:
- Pull-request compatibility report.
- Candidate and verified release-set manifests.
- Version-bound evaluation evidence.
- 75/25 and 0/100 traffic evidence.
- Stable/candidate direct-route responses.
- Rollback closure, restored dashboard metadata, pinned invocation output, and incident link.
Reflect on the release design
- Why is a logical semantic version insufficient to identify a deployed Foundry hosted agent?
- Why must the release set capture an immutable version endpoint and create a version-bound session instead of invoking only a logical agent name?
- Why does the orchestrator deploy after scanner and reviewer even though the specialist agents are independent?
- Why is the dashboard, rather than the Foundry agents, the 75/25 traffic boundary?
- How do direct label URLs make canary verification deterministic?
- Why can't rollback assume the second-newest Container App revision is safe?
- Why does reverse-dependency closure include orchestrator when scanner fails?
- Which release fields must remain identical when promoting reviewed source from staging to production, and which environment-specific Foundry values may differ?
- Why are healthy/regression fixtures secondary to version-bound smoke evaluation evidence?
- Which managed identity invokes Foundry from the dashboard, and why is a stored API key unnecessary?
Optional challenge: Extend the smoke-test dataset
Add a third deterministic smoke record that requires the orchestrator to explain why a release should be held when scanner and reviewer disagree. Increment the dataset version in every agent definition, update the targeted test expectation, and confirm the reviewed release ID changes even though no tool contract changed. Don't weaken the exact model policy or promote without version-bound evidence.
Task 14: Clean up
Delete each lab resource group:
az group delete --name '<development-resource-group>' --yes --no-wait
az group delete --name '<staging-resource-group>' --yes --no-wait
az group delete --name '<production-resource-group>' --yes --no-wait
Then:
- Delete the
development,staging, andproductionGitHub environments. - Delete the automation-owned
verified-development,verified-staging, andverified-productionGitHub releases. - Delete the Entra app registration and Enterprise application if they were created only for this lab.
- Delete the practice repository if you no longer need the evidence.
Live-only assumptions
No live deployment is required to validate the lab source. During an actual learner deployment:
- The selected region must support
gpt-5.4-miniversion2026-03-17with sufficient quota. azdmust returnAGENT_<SERVICE>_NAME,AGENT_<SERVICE>_VERSION, andAGENT_<SERVICE>_RESPONSES_ENDPOINTafter each hosted-agent deployment.- Hosted-agent session and Responses endpoints must accept Microsoft Entra bearer tokens obtained by
DefaultAzureCredential, and session creation must return the concrete requested version. - GitHub environment protection must enforce staging and production approvals.
- Container Apps revision labels and direct label URLs must be enabled by the current Azure CLI/Container Apps API.