CI Deploy Workflows - digitalspace/eagle-demi GitHub Wiki

CI Deploy Workflows (eagle-demi)

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.

Deploy DEMI API to Azure staging

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.

  1. Checkout at fetch-depth: 0 — BUILD_ID comes from git describe --tags, which needs full tag ancestry.
  2. yarn install --immutable.
  3. azure/login@v3 as demi-cicd-test.
  4. 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.
  5. python3 scripts/package-api.py . /tmp/api-flex.zip, then az functionapp deployment source config-zip.
  6. scripts/wait-for-function.sh <resource group> demi-api-fc-test api — polls az functionapp function list for the api route, up to 10 times at 20-second intervals, and exits 1 on timeout.
  7. Smoke test: curl -fsS --retry 3 --retry-delay 5 against https://<default hostname>/health. /health needs no auth and sits outside /api (host.json sets routePrefix: ""); registered is not the same fact as serving.
  8. scripts/search-smoke.sh against the same host: three real queries (Document, Project, Document filtered by a project). The two unfiltered ones must answer 200 with searchResultsTotal > 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.

Deploy DEMI Frontend to Azure staging

Deploys frontend/** — the whole trigger, since yarn --cwd frontend build ships only frontend/dist. Node 24.

  1. Checkout at fetch-depth: 0, kept identical to the API workflow's checkout so the two files cannot drift, though this path stamps no BUILD_ID itself.
  2. scripts/point-env-js.sh test, run against frontend/public/env.js in the source, before the build. Rewrites ENVIRONMENT, KEYCLOAK_URL, NOTIFY_API_LOCATION, configEndpoint (forced true) and APPINSIGHTS_CONNECTION_STRING (from vars.APPINSIGHTS_CONNECTION_STRING_TEST), matching each line by key name rather than by its committed value. Each rewrite is followed by a grep that fails the job if it did not take — sed exits 0 on a no-op match, so an unasserted rewrite would ship the DEV config while reporting success. prod has no default Notify host and fails the script if NOTIFY_API_LOCATION is unset.
  3. yarn --cwd frontend install --immutable.
  4. azure/login@v3 as demi-cicd-test.
  5. ./scripts/deploy-azure.sh frontend <resource-group>, with FRONTEND_STORAGE_ACCOUNT from a repository variable (the account name carries a uniqueString suffix, so it cannot be a literal). Builds the bundle, then uploads to $web in three passes so Cache-Control can be set per blob — $web has 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.

Deploy DEMI secret sync to Azure staging

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.

  1. azure/login@v3 as demi-cicd-test.
  2. scripts/package-secret-sync.sh . /tmp/secret-sync.zip, then az functionapp deployment source config-zip.
  3. scripts/wait-for-function.sh for both triggers by name: secretSyncVaultEvent (the Event Grid subscription that fires on SecretNewVersionCreated) and secretSyncDaily. 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.

Deploy DEMI PDF title worker to Azure staging

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.

  1. azure/login@v3 as demi-cicd-test, in the test environment.
  2. 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.
  3. scripts/package-pdf-title.sh . /tmp/pdf-title.zip, then az functionapp deployment source config-zip.
  4. scripts/wait-for-function.sh for the timer pdf_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.

Deploy DEMI to Azure production

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:

  1. 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-schema on the API serving production at dispatch time (scripts/search-schema-probe.sh), which runs each select and orderby against the live index with top: 0. A 503 — the live index is narrower than the tag about to ship — stops the release before deploy-api runs. A 404 (the running app predates the endpoint) also stops the release, unless the allow_missing_schema_probe dispatch input is ticked — that input is for exactly this case, the rollout that first adds the endpoint. A 500 from either /health/search-schema or /health/search-data is a third case: the probe endpoint itself is broken, not the schema. The skip_schema_probe dispatch input (default false) 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 use skip_schema_probe.

  2. deploy-extractor — checks out refs/tags/<version>, probes whether eagle-extractor-prod exists (measured 2026-09-05: it does not, so the job is a no-op that skips its own steps rather than failing), packages extractor/ and config-zips it, then scripts/wait-for-function.sh for the extract_document trigger.

    Alongside it, deploy-secret-sync checks out the same tag, packages src/secret-sync/** with scripts/package-secret-sync.sh, config-zips it to demi-secret-sync-prod, and waits for both secretSyncVaultEvent and secretSyncDaily to register. See Secret-Sync for what the app does once deployed.

  3. deploy-api (needs: [verify-search-schema, deploy-extractor]) — same tag checkout, yarn workspaces focus --production, config-zips demi-api-fc-prod, then scripts/wait-for-function.sh for the api route, then scripts/search-smoke.sh (three real queries: Document, Project, Document filtered by a project). Ordered behind the extractor deliberately: both jobs sit behind the same prod approval, so without needs: they would start together, and the API would be serving traffic while the extractor feeding it is still coming up.

  4. rollback (needs: deploy-api, runs when deploy-api fails 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.

  5. publish-release (needs: deploy-api, skipped if rollback ran) — flips the tag's draft release to published and marks it --latest, only once deploy-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.

⚠️ **GitHub.com Fallback** ⚠️