Skip to content

Asset Publishing Runbook (Alpha)

Status: Current — verified 2026-08-27. Both referenced pieces of tooling exist: .github/workflows/azure-assets-publish.yml and scripts/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 to PATH on Windows; every step below prepends it).
  • az login against tenant 54754608-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 login after 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-batch returns "You do not have the required permissions" even with --auth-mode login: RBAC propagation lag or a stale token. Either wait ~5 min and az logout && az login to refresh the token, or use --account-key (above).

  • build-asset-manifest.py prints skip: assets/<dir> not present for every folder: you ran it on a checkout that doesn't have the ignored assets/ tree. This is the failure mode of the CI workflow; re-run from a dev box that has the assets.

  • UnicodeEncodeError on Windows console: harmless — the staging succeeded, only the final summary print(...) 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:BaseUrl is missing/blank, so HttpAssetManifestProvider.Find() short-circuits to null and CatalogPackageBuilder falls 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: Migrate assets/ 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.