Repository navigation
Deploy to Azure, behind a green CI run and a human - #35
Merged
Merged
Conversation
Badri-Rajendran
force-pushed
the
feature_container_image
branch
from
September 22, 2026 20:43
1f6dcb9 to
923761c
Compare
Badri-Rajendran
force-pushed
the
feature_azure_cd
branch
from
September 22, 2026 20:43
15ff1a1 to
6d65de9
Compare
Closes the second of ADR 0002's two deferred items, which waited on a subscription that now exists. Container Apps for the API, Static Web Apps for the browser app, and a PostgreSQL Flexible Server. The choices that are not obvious: Migrations run as a Container Apps job on the same image digest that is about to serve traffic. Running Alembic from the GitHub runner instead would mean opening the database to GitHub's IP ranges for the sake of a deploy step; this keeps the server private and guarantees the schema is migrated by exactly the code that will then use it. The image is tagged with the commit and deployed by digest. A tag can be moved, a digest cannot, so what the reviewer approves is what runs. The trigger is `workflow_run` on a successful CI run, which makes green CI a precondition rather than a convention, and `environment: production` with a required reviewer is the approval. A post-deploy check polls the API until it answers 200, so a revision that starts but cannot serve fails the run. Sign-in is OIDC federated credentials, so no Azure secret is stored in GitHub at all. The commit a manual run asks for is checked against a 40-hex pattern before it reaches `ref:` or an image tag. One replica, deliberately: Flask-Limiter keeps its counters in process, so a second would double the 5/min on register and 10/min on login. Raising it and adding a shared store are the same piece of work. The database is seeded once by hand from a 75 MB dump, not by CI. Ingestion stays on the machine that holds data/ (ADR 0016, 0019), and nothing here uploads a PDF. docs/runbooks/deploy.md covers the one-time setup, the deploy path, rollback, and the failures worth expecting. What is still missing — staging, alerting, a shared rate-limit store — is named rather than left to be discovered.
Badri-Rajendran
force-pushed
the
feature_azure_cd
branch
from
September 22, 2026 20:59
6d65de9 to
ac841d8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
CLAUDE.mdrequires: "CD: push images to Azure Container Registry, run Alembic migrations, deploy to Azure" and "Gate production deploys on green CI plus manual approval; keep rollback a single re-run."docs/decisions/0002-ci-scope.md:37-41deferred it: "Requires an Azure subscription, service principal, and GitHub Environment secrets that don't exist in this project yet." A subscription now exists. This is the last of that ADR's three deferrals.Implementation
Topology (ADR 0023): Container Apps for the API, Static Web Apps for the Vite build, PostgreSQL Flexible Server with
pgvector.The choices that are not obvious:
--public-access None, and guarantees the schema is migrated by exactly the code that will then use it.workflow_runon a successful CI run is the trigger, so "green CI" is a precondition, not a convention — a push that fails CI never reaches CD.environment: productionwith a required reviewer is the manual approval. Nothing reaches Azure until a person says so./api/counties?zip=until 200, so a revision that starts but cannot serve fails the run rather than sitting there.id-token: writeat the workflow level,contents: readotherwise.--max-replicas 1as load-bearing.The PDFs stay put. ADR 0016 and ADR 0019 make ingestion a local operation; nothing in this workflow uploads
data/. The database is seeded once by hand from a 75 MBpg_dump— the text, the chunks, the catalog and the BM25 row (ADR 0021), not the documents.Rollback is
az containerapp revision activate <previous>— one command, which is what ADR 0002 asked for.Validation
yaml.safe_load).uv run pytest -q— 415 passing;ruff checkclean. No application code changed.inputs.shaonworkflow_dispatchis typed by a human and flows intoref:and an image tag, so it is checked against^[0-9a-f]{40}$first — a ref-injection vector otherwise. Everyazargument comes from repository variables throughenv:, never interpolated into a shell line.docs/runbooks/deploy.mdcovers one-time setup, the per-merge path, rollback, a failed migration, a 500 after deploy, and cold starts.Not verified against a live subscription — I have no credentials, and would not create cloud infrastructure unprompted. The workflow is written to the documented APIs; the first run is the real test, and the runbook is written for someone doing it.
Security / DB / API impact
secretref:on the Container App, never in the repo; image deployed by immutable digest; deploy gated on a human.Still missing, named rather than discovered later
No staging environment, no alerting, and no shared rate-limit store — which is what pins the app at one replica. All three are in ADR 0023's consequences.
Note
Top of the stack: #34 → #33 → #32 → #31 → #30 → #29 → #28.