For operations
Two identities, three jobs, one gate
The interesting engineering here is not the upload. It is the separation between the identity that writes blobs and the identity that clears cache, and the refusal to report success until the public URL proves it.
What a tenant workflow contains
A thin caller: hostname, caller repository, the upload client id, the project prefix, and optionally the apex flag and environment list. It pins our library by tag. It contains no storage account name, no Front Door name, and no purge identity.
Cache headers, hashing mode, and the unhashed path list are library defaults. If they appear in a tenant file, that is a smell, not a feature.
The caller and library use the current Node 24 generations of GitHub's checkout, artifact, and Azure login actions. Runtime updates stay in the maintained workflow instead of becoming work for every content repository.
Deploy job — the upload identity
- Federate Token for that repository and branch. Blob data role plus conditions limiting write, delete, add, and move to the project prefix.
- Stage Fingerprint the tree, rewrite HTML references, and write
state/version.txtwith the UTC instant, git SHA, and deploy date. That marker is read from storage, never through a public URL. - Queue first Read every unfinished queue, coalesce pending paths with this deploy’s durable paths and any dropped unhashed paths, then write and byte-verify exactly one dated queue before changing
public/. - Hashed first Long-lived objects go up before anything can reference them.
- Unhashed last Index pages and explicit durable extras go up only after the queue is durable. Dropped durable blobs are also removed only after that point.
Ordering is the point: a request landing mid-deploy either sees old HTML with assets that still exist, or new HTML with assets that already exist. There is no window where a page references something absent.
This job runs no Front Door commands at all — no CDN CLI extension, no route update. The route still accepts inbound /*, while its fixed originPath is /<PROJECT_ID>/<fqdn>/public. Visitor files live at $web/<PROJECT_ID>/<fqdn>/public/; workflow state is its sibling state/.
Purge job — the platform identity
A separate reusable workflow, and the only file the directory will issue that token for. The federated credential is bound to the workflow reference and the organization, using the immutable subject format that includes numeric organization and repository ids.
Its due diligence, in order:
- Caller Taken from the platform, not from an input. The repository name is the production hostname.
- Index
PROJECT_IDselects provisioned Front Door routes. A wrong value is a denial, never a grant. - Membership The caller repository name must be a custom domain on a route whose exact origin path is
/<PROJECT_ID>/<fqdn>/public. - Grant Exactly one FQDN is accepted per purge, and it must pass that same custom-domain and origin-path test. Tenant-writable storage folder names are not authorization.
Once membership is proven, the trusted platform purge workflow may authorize a production-triggered purge of preview or the reverse. Isolation is between sites, not between one site’s environments. The uploader remains limited by storage ABAC and has no Front Door permission.
The durable queue
Queue bodies contain only FQDN-relative Front Door paths such as /, /index.html, and /blog/*—never a hostname or storage path. The filename keeps the original unfinished date: state/to-purge-YYYYMMDD.txt. A purge or verification failure leaves it in place; only a successful Front Door purge followed by successful public verification removes it.
Front Door accepts at most 100 path expressions. If a union is larger, the queue rolls up the deepest, smallest overlapping directory group one level at a time to /dir/*, retaining /dir/ when that index cache key is separate. The result covers the full original set in one purge request; it never truncates at 100 or leaves a second batch waiting. Ordinary deploys reach /* only when no narrower deterministic result can fit.
One practical constraint worth writing down: a nested reusable workflow reference cannot be built from the calling run’s commit, so both the parent library and the purge workflow are pinned to the same moving tag.
Verify job — the gate
Each round fetches every unconfirmed durable URL in parallel. A current path passes only when it returns 200 and the SHA-1 of its decoded response body matches the SHA-1 of the staged file; an exact dropped path passes when it returns 404. One matching endpoint response confirms each path, while the log states that the full global rollout may still be proceeding. No certificate verification is skipped.
Both /dir/ and /dir/index.html are checked because Front Door treats them as separate cache keys even though they resolve to the same file. Fingerprinted names are deliberately absent from deploy-time prune, purge, and verification: changed bytes use new URLs, and old origin copies remain for the separate lifecycle job. Because each custom domain is its own cache, a production purge is not evidence about preview. Only after this gate passes may the workflow byte-compare and delete the unchanged queue file.
Permissions a tenant identity must never hold
| Withheld | Reason |
|---|---|
| Cache purge on the shared endpoint | The domain list is a request field, so it cannot be constrained by access control. |
| Profile read used only to enable those commands | Removes the path entirely rather than trusting intent. |
| Route and custom domain write | Origin paths and TLS belong to provisioning, not to a deploy. |
| Anything outside its own prefix | Enforced by attribute conditions on the blob role. |
Certificates, DNS, and the parts we do not automate from CI
Creating a custom domain produces a validation token; someone publishes it as a TXT record, and the platform polls until the domain is approved. Certificates are managed. There is a second TLS hop from the edge to the storage website endpoint. The profile’s default hostname is expected to return 404.
Known and accepted in this build: the static website endpoint is publicly reachable, so a known prefix can be fetched directly, bypassing the edge. That is obscurity, not a network boundary, and there is no firewall product in front of it at this tier.
Storage lifecycle cleanup—not cache invalidation
This planned cost-control job will walk only each hostname’s public/ subtree; it is not another name for Front Door purge. It will read state/version.txt directly from storage and abort when that marker is missing or unparseable. It never touches state/to-purge-*: queues are operational evidence, are not lifecycle-swept, and produce a warning when they are more than about 30 days old. Objects dated to the current deploy are protected absolutely; only fingerprinted public blobs older than two weeks become deletion candidates.
Current feature state: automatic deploy uses the durable pre-mutation queue, route-inventory authorization, precise Front Door purge, SHA-1 body verification, dropped-path 404 checks, and unchanged-queue deletion. Tenant Purge all is available as the manual recovery path. The separate storage lifecycle workflow remains planned, so old fingerprinted blobs accumulate. Blob soft delete is enabled for seven days; the Terraform design specifies fourteen days of recovery after lifecycle deletion.
Deeper: the live invalidation identity, two identities in detail, the two-apply DNS flow, the planned storage lifecycle rules.