Asset Publishing Runbook (Alpha)¶
Status: Current — verified 2026-08-27. Both referenced pieces of tooling exist:
.github/workflows/azure-assets-publish.ymlandscripts/build-asset-manifest.py. Owner: delan
How to publish runtime assets to the alpha Azure blob container so the API
can serve CatalogPackage entries with content-hashed URLs.
See PackageModel.md §3 for the contract.
Why this is a manual stop-gap¶
The repo's assets/ tree is gitignored (see .gitignore), so the
azure-assets-publish.yml GitHub Actions workflow runs build-asset-manifest.py
against an empty checkout and uploads only manifest.json + the few
non-ignored files. Until assets ship via Git LFS (planned: feat/assets-via-lfs),
publishes must be initiated from a developer workstation that has the full
assets/ tree.
Target¶
- Subscription:
6bdab9fc-9cda-4c5e-8d8c-ad7c5edb4c3f(Azure subscription 1) - Resource group:
rg-echospire-alpha - Storage account:
esalphastvzucst7q - Container:
assets(public blob read) - Container App consuming it:
es-alpha-api - Env var the API reads:
Assets__BaseUrl - Manifest URL:
https://esalphastvzucst7q.blob.core.windows.net/assets/manifest.json
Pre-reqs¶
- Azure CLI installed at
C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin(the installer does NOT add this toPATHon Windows; every step below prepends it). az loginagainst tenant54754608-6a40-42fb-a686-698dbf342353.- Either:
- Owner on the storage account (lets you read keys), or
- Storage Blob Data Contributor on the storage account and a fresh
token (re-
az loginafter the role is granted — RBAC propagation + cached token will otherwise produce 403s for several minutes). - Python 3.11+ on
PATH.
Publish procedure¶
# 0. Ensure az CLI is on PATH (one-time per shell)
$env:PATH = "C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin;$env:PATH"
# 1. (Recommended) Re-audit and archive any newly-unused files. Bulk
# additions should land via the artifacts/asset-audit workflow first.
python scripts\audit-assets.py --repo-root "$PWD" `
--report artifacts\asset-audit.md --json artifacts\asset-audit.json
python scripts\archive-unused-assets.py # dry-run
python scripts\archive-unused-assets.py --apply # commits the move
# 2. Build content-hashed staging tree + manifest
$ver = "alpha-$(Get-Date -Format yyyyMMddHHmm)"
python scripts\build-asset-manifest.py `
--repo-root "$PWD" --staging .asset-staging --version $ver
# 3. Upload to blob. Account-key auth is the lowest-friction path; the key
# never leaves the shell. Do NOT echo $key.
$key = az storage account keys list `
-g rg-echospire-alpha -n esalphastvzucst7q `
--query "[0].value" -o tsv
az storage blob upload-batch `
--account-name esalphastvzucst7q --account-key $key `
--destination assets --source .asset-staging `
--overwrite --no-progress
Expected upload size: ~1,051 hashed asset files + manifest.json
(~506 MiB at time of writing). Runtime is dominated by the 28 mp3 loops
under sounds/samples/*/loops/mp3/ (~120 MiB).
Re-runs are cheap: content-hashed blob names are immutable, so unchanged
files no-op when --overwrite is omitted. We pass --overwrite only so
manifest.json itself refreshes.
Verify¶
$env:PATH = "C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin;$env:PATH"
$key = az storage account keys list -g rg-echospire-alpha -n esalphastvzucst7q --query "[0].value" -o tsv
# Blob count (expect ~1,051+1 manifest)
(az storage blob list --account-name esalphastvzucst7q `
--container-name assets --account-key $key `
--num-results 5000 --query '[].name' -o tsv).Count
# Manifest reachable publicly
Invoke-WebRequest -Method Head `
"https://esalphastvzucst7q.blob.core.windows.net/assets/manifest.json"
# → 200, Content-Type: application/json
Wire it to the API¶
If Assets__BaseUrl is already set on es-alpha-api, no action — the API
will pick up new manifest contents on its next refresh interval (5 min
default; see AssetManifestOptions.RefreshInterval). First-time setup:
$env:PATH = "C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin;$env:PATH"
az containerapp update -g rg-echospire-alpha -n es-alpha-api `
--set-env-vars "Assets__BaseUrl=https://esalphastvzucst7q.blob.core.windows.net/assets/"
Setting this env var creates a new revision; the previous one is drained automatically.
Smoke test:
# 401 from an unauthenticated request confirms the route is live.
Invoke-WebRequest -UseBasicParsing `
"https://es-alpha-api.jollypond-e4914711.westus3.azurecontainerapps.io/api/v1/packages/catalog"
# With a bearer token, the response's AssetBaseUrl + AssetManifest[].RelativePath
# should compose to the public blob URLs above.
Troubleshooting¶
-
upload-batchreturns "You do not have the required permissions" even with--auth-mode login: RBAC propagation lag or a stale token. Either wait ~5 min andaz logout && az loginto refresh the token, or use--account-key(above). -
build-asset-manifest.pyprintsskip: assets/<dir> not presentfor every folder: you ran it on a checkout that doesn't have the ignoredassets/tree. This is the failure mode of the CI workflow; re-run from a dev box that has the assets. -
UnicodeEncodeErroron Windows console: harmless — the staging succeeded, only the final summaryprint(...)crashed. The script was patched to use ASCII; pull latest if you still see it. -
API responses still show repo-relative paths instead of hashed blob paths:
Assets:BaseUrlis missing/blank, soHttpAssetManifestProvider.Find()short-circuits tonullandCatalogPackageBuilderfalls back to the literal path. Confirm the env var on the active revision:
powershell
az containerapp revision show -g rg-echospire-alpha -n es-alpha-api `
--revision <name> -o json | ConvertFrom-Json |
ForEach-Object { $_.properties.template.containers[0].env } |
Where-Object { $_.name -like 'Assets*' }
Follow-up work¶
feat/assets-via-lfs: Migrateassets/to Git LFS so the GitHub Actions workflow can publish autonomously and this runbook becomes obsolete.- 181 broken asset references (mostly enemy + consumable art that hasn't been generated yet) are tracked separately; they don't block publishes.