CI Deploy Workflows - digitalspace/eagle-demi GitHub Wiki
Reference for the eagle-demi workflows that deploy to Azure: staging API, frontend, secret sync,
PDF title worker, and production. The workflow index, PR validation and repository settings are
CI-Workflows.
Deploys src/**, api/**, index.js, host.json, package.json, yarn.lock and
frontend/public/assets/geojson/** (the boundary seeder reads the GeoJSON at runtime, so that one
path under frontend/ belongs to the API package and fires this workflow too) to the Flex
Consumption app demi-api-fc-test. Node 22.
- Checkout at
fetch-depth: 0—BUILD_IDcomes fromgit describe --tags, which needs full tag ancestry. -
yarn install --immutable. -
azure/login@v3asdemi-cicd-test. -
yarn workspaces focus --production— drops dev dependencies from the package before it is built; the Flex extract phase is paid per file, and nothing after this step needs them. -
python3 scripts/package-api.py . /tmp/api-flex.zip, thenaz functionapp deployment source config-zip. -
scripts/wait-for-function.sh <resource group> demi-api-fc-test api— pollsaz functionapp function listfor theapiroute, up to 10 times at 20-second intervals, and exits 1 on timeout. - Smoke test:
curl -fsS --retry 3 --retry-delay 5againsthttps://<default hostname>/health./healthneeds no auth and sits outside/api(host.jsonsetsroutePrefix: ""); registered is not the same fact as serving. -
scripts/search-smoke.shagainst the same host: three real queries (Document, Project, Document filtered by a project). The two unfiltered ones must answer 200 withsearchResultsTotal > 0; the filtered one only needs 200. No rollback here — staging is where a bad build is supposed to stop. Runbook-Search-Outage covers what to do when this or the live index disagree.
Identity: UAMI demi-cicd-test, federated subject repo:digitalspace/eagle-demi:environment:test
(the GitHub environment: test block is what produces that subject — renaming the environment
without a matching federated credential fails Azure Login with AADSTS700213). Holds Website
Contributor on demi-api-fc-test only, nothing at resource-group scope.
Deploys frontend/** — the whole trigger, since yarn --cwd frontend build ships only
frontend/dist. Node 24.
- Checkout at
fetch-depth: 0, kept identical to the API workflow's checkout so the two files cannot drift, though this path stamps noBUILD_IDitself. -
scripts/point-env-js.sh test, run againstfrontend/public/env.jsin the source, before the build. RewritesENVIRONMENT,KEYCLOAK_URL,NOTIFY_API_LOCATION,configEndpoint(forcedtrue) andAPPINSIGHTS_CONNECTION_STRING(fromvars.APPINSIGHTS_CONNECTION_STRING_TEST), matching each line by key name rather than by its committed value. Each rewrite is followed by agrepthat fails the job if it did not take —sedexits 0 on a no-op match, so an unasserted rewrite would ship the DEV config while reporting success.prodhas no default Notify host and fails the script ifNOTIFY_API_LOCATIONis unset. -
yarn --cwd frontend install --immutable. -
azure/login@v3asdemi-cicd-test. -
./scripts/deploy-azure.sh frontend <resource-group>, withFRONTEND_STORAGE_ACCOUNTfrom a repository variable (the account name carries auniqueStringsuffix, so it cannot be a literal). Builds the bundle, then uploads to$webin three passes soCache-Controlcan be set per blob —$webhas no way to add a response header.
Identity: same UAMI, demi-cicd-test. Holds Website Contributor on demi-api-fc-test, plus
Storage Blob Data Contributor (publish the bundle) and Storage Account Contributor (turn
on static website hosting, a service-properties write Blob Data Contributor does not carry) on the
$web storage account, and nothing wider.
Deploys src/secret-sync/** (plus src/utils/** and src/config.js, which the sync packages
alongside its own code for the shared logger and its config) to the Flex Consumption app
demi-secret-sync-test. What that app does with the secrets it copies is Secret-Sync; this
workflow only ships its code.
-
azure/login@v3asdemi-cicd-test. -
scripts/package-secret-sync.sh . /tmp/secret-sync.zip, thenaz functionapp deployment source config-zip. -
scripts/wait-for-function.shfor both triggers by name:secretSyncVaultEvent(the Event Grid subscription that fires onSecretNewVersionCreated) andsecretSyncDaily. The Event Grid subscription points at this function by name, so a deploy that registers neither trigger leaves rotations silently undelivered with no other signal.
Identity: same UAMI, demi-cicd-test.
Deploys pdf-title/ to the Flex Consumption app demi-pdf-title-test. What the app does is
PDF-Titles; where it is defined is Operating-DEMI-in-Azure. This workflow only ships its code.
-
azure/login@v3asdemi-cicd-test, in thetestenvironment. - Probe the app with
az functionapp show. If the app does not exist, or CI lacks Website Contributor on it, the workflow prints a notice and skips the later steps. Any other error fails the run. -
scripts/package-pdf-title.sh . /tmp/pdf-title.zip, thenaz functionapp deployment source config-zip. -
scripts/wait-for-function.shfor the timerpdf_title_run. A host that registers no function loads without error, so the wait is the only signal.
Identity: same UAMI, demi-cicd-test. Its Website Contributor role on demi-pdf-title-test is
granted by hand, per app.
workflow_dispatch only, taking a version input shaped vX.Y.Z. No push: trigger, and there
must never be one — a prod deploy is dispatched deliberately, not a side effect of a merge.
Six jobs. deploy-secret-sync carries no needs: and runs independently of the rest — it writes
secrets into 6cdc9e-prod and reads nothing the API serves, so it neither blocks nor is blocked by
the API deploy. The other five run in order:
-
verify-search-schema — checks out the tag being deployed, builds a probe body from that tag's
azure/search/indexes/*.json, and POSTs it to/health/search-schemaon the API serving production at dispatch time (scripts/search-schema-probe.sh), which runs each select and orderby against the live index withtop: 0. A 503 — the live index is narrower than the tag about to ship — stops the release beforedeploy-apiruns. A 404 (the running app predates the endpoint) also stops the release, unless theallow_missing_schema_probedispatch input is ticked — that input is for exactly this case, the rollout that first adds the endpoint. A 500 from either/health/search-schemaor/health/search-datais a third case: the probe endpoint itself is broken, not the schema. Theskip_schema_probedispatch input (defaultfalse) is break-glass for that: it skips both the schema and the data probe, logs a warning naming the dispatcher and the tag, records the skip in the release notes, and lets the rest of the deploy run normally. This is the pre-deploy half of the guard added after the 2026-09-08 outage; Runbook-Search-Outage covers the response when it fires, when drift reaches production anyway, and when to useskip_schema_probe. -
deploy-extractor — checks out
refs/tags/<version>, probes whethereagle-extractor-prodexists (measured 2026-09-05: it does not, so the job is a no-op that skips its own steps rather than failing), packagesextractor/andconfig-zips it, thenscripts/wait-for-function.shfor theextract_documenttrigger.Alongside it, deploy-secret-sync checks out the same tag, packages
src/secret-sync/**withscripts/package-secret-sync.sh,config-zips it todemi-secret-sync-prod, and waits for bothsecretSyncVaultEventandsecretSyncDailyto register. See Secret-Sync for what the app does once deployed. -
deploy-api (
needs: [verify-search-schema, deploy-extractor]) — same tag checkout,yarn workspaces focus --production,config-zipsdemi-api-fc-prod, thenscripts/wait-for-function.shfor theapiroute, thenscripts/search-smoke.sh(three real queries: Document, Project, Document filtered by a project). Ordered behind the extractor deliberately: both jobs sit behind the sameprodapproval, so withoutneeds:they would start together, and the API would be serving traffic while the extractor feeding it is still coming up. -
rollback (
needs: deploy-api, runs whendeploy-apifails after the app was published (smoke failed or the app never came up); pre-publish failures leave the old release in place and skip rollback) — redeploys the newest published release that is not the one just deployed, through the same composite action the forward deploy uses (.github/actions/deploy-api-flex), re-runs the smoke test, and leaves the workflow run red with the new release left unpublished. -
publish-release (
needs: deploy-api, skipped ifrollbackran) — flips the tag's draft release to published and marks it--latest, only oncedeploy-api's smoke test is green. See Release-Process for why publishing is the last step, not the first.
Identity: UAMI demi-cicd-prod, federated subject
repo:digitalspace/eagle-demi:environment:prod, used by every job above including
deploy-secret-sync. Holds Website Contributor on demi-api-fc-prod only (the extractor and
the secret sync app get the same grant once each is stood up); deploy-secret-sync probes whether
demi-secret-sync-prod exists first and skips its own steps rather than failing when it does not,
the same pattern deploy-extractor uses. Kept off any
runtime identity on purpose: eagle-demi is a public repository, and a federated credential hung off
the runtime UAMI would let any workflow in it mint a token with Cosmos and AI Search data access.
GitHub prod environment settings, read from the live repository (2026-09-05): a custom
deployment branch/tag policy allows only the tag pattern v* — a dispatch naming a branch, or a
tag that does not match, is refused before the runner starts. There is no required reviewer
configured; the environment relies on the tag-pattern gate alone, and an admin can bypass
even that.