[!IMPORTANT] Every fixture, rulebook, and calculator output in this lab is synthetic and non-binding. Nothing here represents an actual Desjardins product, rate, or policy, and no regulator or insurer has reviewed or endorsed this material.
Overview
| Item | Value |
|---|---|
| Duration | 40 minutes |
| Level | Advanced |
| Prerequisites | Lab 13 |
You have now seen every component: the calculator, the state machine, the MCP servers, the agent, the applicant chat, and the reviewer interface. This lab covers what keeps them honest between changes.
Learning Objectives
By the end of this lab, you will be able to:
- Run the same checks CI runs, locally and offline
- Read the deterministic evaluation gate and explain what it refuses to let through
- Explain why the pipeline authenticates with OIDC and carries almost no secrets
- Describe the safety design of the teardown workflow
- Explain why some resources have to be deleted rather than reconfigured
- Tear down every Azure resource the pilot created, locally with
azdor through a gated workflow
Exercises
Exercise 14.1: Inventory the Pipeline
Get-ChildItem .github/workflows -Filter *.yml | Select-Object -ExpandProperty Name
Expected result: nine workflows.
| Workflow | Trigger | Purpose |
|---|---|---|
continuous-validation.yml | push, pull request, dispatch | Offline regression tests and Bicep compile |
reviewer-app-build.yml | path-filtered push and pull request | Reviewer backend and frontend tests, image build |
web-chat-build.yml | path-filtered push and pull request | Chat backend and frontend tests, image build |
publish-test-trends.yml | after validation completes | Publishes test evidence to the wiki |
deploy-and-evaluate.yml | call and dispatch only | Provision and deploy, staging then production |
hosted-agent-cd.yml | call and dispatch only | Hosted agent deployment |
reviewer-app-teardown.yml | dispatch only | Removes reviewer-scoped resources |
network-rebuild-teardown.yml | dispatch only | Removes resources whose network configuration is immutable |
full-teardown.yml | dispatch only | Deletes whole resource groups at the end of the workshop |
Only the first three run automatically. Nothing that touches Azure runs on a push.
Exercise 14.2 (Hands-on): Run the Offline Suite Locally
These are the same commands continuous-validation.yml runs.
pytest src/quote-preparation-agent/tests -v
pytest eval -v
pytest apps/workshop/tests mcp/application-server/tests mcp/rulebook-server/tests -v
Expected result: all three suites pass. The application and reviewer surfaces are tested by their own workflows:
python -m pytest apps/reviewer-app/tests -v
$env:PYTHONPATH = 'apps/web-chat'
python -m pytest apps/web-chat/tests -v
The reviewer tests insert their own import path, while the chat tests rely on PYTHONPATH. The workflows differ in exactly the same way, which is worth noticing before you copy a command between them.
Exercise 14.3 (Hands-on): Read the Evaluation Gate
python eval/evaluation_gate.py
Expected result: a line reporting how many golden records passed, a bilingual parity result, and a final verdict.
13/13 records passed; bilingual_parity=PASS
Gate: PASS
The gate is deterministic. It calls no model and makes no network request, so a failure always means a behaviour change rather than a flaky provider.
Bilingual parity is the check worth pausing on. A change that improved the English message while leaving the French one stale would pass every unit test in this repository and still be a defect, because both locales are the product. The gate treats a parity break as a failure.
Exercise 14.4: Understand the Credential Posture
Select-String -Path .github/workflows/*.yml -Pattern "secrets\." | Select-Object -ExpandProperty Line
Expected result: the only secret referenced across the pipeline is the wiki push token. Everything else comes from repository variables.
Azure access uses workload identity federation:
Select-String -Path .github/workflows/deploy-and-evaluate.yml -Pattern "azure/login|client-id|tenant-id|subscription-id" | Select-Object -ExpandProperty Line
Expected result: azure/login configured from vars.AZURE_CLIENT_ID, vars.AZURE_TENANT_ID, and vars.AZURE_SUBSCRIPTION_ID. There is no client secret anywhere, because OIDC exchanges a short-lived GitHub token for an Azure token at run time. A leaked repository variable is a set of identifiers, not a credential.
This is also why Lab 12 had to be run by an administrator. The federated identity holds Azure resource permissions and no Microsoft Graph permissions at all.
Exercise 14.5 (Hands-on): Lint the Workflows
actionlint
Expected result: exit code 1 with exactly five findings, all reporting unexpected key "queue":
| File | Line |
|---|---|
deploy-and-evaluate.yml | 53 |
full-teardown.yml | 77 |
network-rebuild-teardown.yml | 78 |
publish-test-trends.yml | 49 |
reviewer-app-teardown.yml | 80 |
These are expected. concurrency.queue is valid to this repository’s deployment model and unrecognized by the linter’s schema. Treat any sixth finding as a real one, and leave these five alone.
Exercise 14.6: Read the Teardown Safety Design
Teardown is the most dangerous workflow in any pilot, so read its header before reading its steps.
Get-Content .github/workflows/reviewer-app-teardown.yml -TotalCount 44
Expected result: a banner explaining that the workflow never deletes the resource group.
The reason is specific. vars.AZURE_RESOURCE_GROUP names a single resource group shared by both the staging and production environments, so az group delete issued while tearing down staging would take production with it. The workflow therefore deletes individually named reviewer-scoped resources only, and a protected-name guard fails the run if a computed deletion target ever collides with shared infrastructure.
Four independent safeguards sit in front of the first Azure call:
- The workflow is dispatch only, never push, schedule, or pull request
- The operator must type the target environment name into a
confirminput, and a mismatch fails before any Azure call executedefaults to false, so a run with no inputs changed is a dry run that reports without deleting- The job binds to the matching GitHub Environment, so required-reviewer protection applies
Every delete is existence-checked, so a rerun after a partial failure succeeds and a run against already-absent resources exits 0.
Exercise 14.7: Read the Network Rebuild Teardown
Some configuration cannot be changed in place. vnetConfiguration on a Container Apps managed environment and networkInjections on a Foundry account are both set only at creation, so moving an already-deployed environment onto a virtual network means deleting it first.
Get-Content .github/workflows/network-rebuild-teardown.yml -TotalCount 46
Expected result: a banner naming the three resource kinds it deletes and, at greater length, what it preserves.
The preserved list is the more interesting half. The Cosmos accounts stay, because a private endpoint attaches to an existing account and deleting them would discard every case in the store. The virtual network stays, because both environments share it. The Entra registrations stay, because one reviewer registration serves staging and production together.
Select-String -Path .github/workflows/network-rebuild-teardown.yml -Pattern "seq 1 30|purgeable" | Select-Object -ExpandProperty Line
Expected result: a polling loop around the Foundry account purge.
That loop exists because of a timing detail that is easy to get wrong. az cognitiveservices account delete returns success while the account is still in a Deleting state, and a purge issued during that window is rejected. An injected account also holds a service association link on its subnet until the purge completes, so a single purge attempt leaves the subnet pinned and the rebuild blocked.
[!WARNING] Deleting a managed environment changes the FQDN of every app inside it. The redirect URIs you registered in Lab 12 and the chat registration from Lab 11 both need refreshing afterwards, using the same scripts. Both scripts merge redirect URIs rather than replacing them, so rerunning them is safe.
Exercise 14.8 (Hands-on): Tear Everything Down
When you have finished the workshop, delete the Azure resources so they stop incurring cost. List your azd environments and the resource group each one points at:
azd env list
azd env get-value AZURE_RESOURCE_GROUP -e <environment>
In the reference deployment there are two groups:
| Resource group | Contents |
|---|---|
rg-desjardins-quote-preparation | Staging and production: Foundry, Container Apps, Cosmos DB, the container registry, and the virtual network |
rg-desjardins-quote-preparation-poc | An earlier proof-of-concept deployment |
[!CAUTION]
rg-desjardins-quote-preparationholds staging and production together, which is exactly why Exercise 14.6’s workflow refuses to delete it. Deleting it ends the pilot for everyone. The GitHub OIDC identity’s role assignments are scoped to that group and disappear with it, so a later rebuild needs a subscription administrator to recreate the group and re-grant those roles.
Option A: locally with azd. This uses your own Azure sign-in, so you need Contributor on the group.
azd down --force --purge -e <environment>
--force skips the confirmation prompt and --purge permanently removes the soft-deleted Foundry account and Log Analytics workspaces, so their names are free for a future deployment. If azd down reports nothing to delete (for example, because the environment was provisioned by the pipeline rather than from your machine), delete the group directly and purge the Foundry account yourself:
az group delete --name <resource-group> --yes
az cognitiveservices account list-deleted --output table
az cognitiveservices account purge --name <account> --resource-group <resource-group> --location <location>
Option B: through the pipeline. full-teardown.yml is the third teardown workflow. Start with a dry run, which only inventories each group in the run summary:
gh workflow run full-teardown.yml -f target=poc -f confirm=poc
gh workflow run full-teardown.yml -f target=poc -f confirm=poc -f execute=true
Expected result: each run waits for approval on the production environment. The dry run summary lists every resource type in the group; the second run deletes it. Use target=shared or target=both to include the staging and production group.
The workflow keeps the same four safeguards as the other teardown workflows and adds a fifth: resource group names must match an allowlist pattern, so a mistyped repository variable cannot point it at an unrelated group. It also purges each Foundry account before deleting the group, because the identity’s roles are scoped to the group and would be gone by the time a purge ran afterwards.
[!NOTE] By default the workflow identity has access to
vars.AZURE_RESOURCE_GROUPonly. Apocrun reports the proof-of-concept group as not accessible and skips it with a warning; either grant the identity Contributor on that group first or use Option A for it. The workflow is safe to rerun: when the groups are already gone, it succeeds with a warning and deletes nothing. The Entra app registrations are not deleted by either option; remove the reviewer registration withscripts/remove-reviewer-identity.ps1.
Validation Checklist
- You listed all nine workflows and identified which run automatically
- Every local test suite passes
python eval/evaluation_gate.pyreportsGate: PASS- The only secret in the pipeline is the wiki push token
actionlintreports exactly five knownqueuefindings- You can name the four safeguards in front of the teardown workflows
- You can explain why the network rebuild teardown preserves the Cosmos accounts
- You deleted the resource groups you no longer need, with
azd downorfull-teardown.yml
Knowledge Check
- Why would a bilingual parity failure be invisible to the unit test suites?
- Why is a repository variable an acceptable home for
AZURE_CLIENT_IDwhen a client secret would not be? - The teardown workflow deletes an AcrPull role assignment but never the container registry. Why?
- If
executedefaults to false, what does a first run of the teardown workflow actually produce? - Why does the deterministic evaluation gate avoid calling a model?
- Why does the Foundry account have to be purged rather than merely deleted before the rebuild can proceed?
- Why does
full-teardown.ymlpurge the Foundry account before deleting the resource group rather than after?
Next Steps
You have completed the bilingual quote-preparation workshop, from a synthetic fixture on your laptop to a reviewed decision with an audit trail.
Return to the labs index or the workshop home page.