| Script / file | When to use |
|---|---|
docker-compose.local.yml |
Minimal 2-sidecar (api + frontend) sanity check. Faster boot; no Celery. |
docker-compose.full.yml |
Full 6-sidecar mirror of the bundled Container App. Use this for any debugging that touches Celery, Redis, the terminal exec channel, or cross-sidecar wiring. |
compose-with-log.sh |
Docker Compose wrapper used by local-run.sh compose-full/compose-local; captures compose output and detached container logs. |
quick-deploy.sh |
One-sidecar image bump on the live Azure Container App. ~30-90 s per cycle vs. 5-10 min for a full Bicep redeploy. Terminal deploys reuse a content-hashed elb-terminal-base toolchain image. |
cli-upgrade.sh |
Safe git pull + build + rolling-update envelope around quick-deploy.sh / postprovision.sh. Snapshots the current revision's image refs before any PATCH, polls /api/health, and auto-rolls back on failure. Also has a rollback mode for emergency recovery. Full guide: docs/operate/cli-upgrade.md. |
postprovision.sh |
Full first-time / structural deploy — sidecar layout, env vars, secrets, probes, scale rules. Run via azd up or directly after sourcing /tmp/azd-env.sh. |
smoke_api.py |
HTTP smoke test against a running api sidecar. |
preflight-check.sh |
Pre-azd up sanity. |
install-git-hooks.sh |
Install the CI-mirror git hooks (sets core.hooksPath=scripts/dev/git-hooks). Run once per fresh clone. pre-commit = ruff + docs frontmatter guard on staged files; pre-push = pytest + mkdocs build --strict scoped to the pushed paths. Mirrors .github/workflows/test.yml and docs.yml. Bypass with --no-verify or ELB_SKIP_HOOKS=1. |
git-hooks/ |
The hook scripts themselves (pre-commit, pre-push, shared _lib.sh). Edit these when the CI workflows' checks or paths: filters change so the local mirror stays accurate. |
setup-app-registration.sh |
One-shot Entra ID app registration creation. |
grant-local-rbac.sh |
One-shot: grant your az login user the minimum RBAC (Storage Blob Data Contributor, Storage Account Contributor, RG Reader, AcrPull) needed to drive a deployed environment from a local api sidecar. Idempotent; run once per fresh clone. |
storage-public-access.sh |
Manually flip a workload Storage account's publicNetworkAccess on (caller IPv4 allowlisted, defaultAction=Deny, bypass=None) / off for local debugging. Normal local launches never open it; the api auto-open path requires an explicit LOCAL_DEBUG_AUTO_OPEN_STORAGE=true opt-in. See api/services/storage/public_access.py. |
local-run.sh |
Direct terminal and VS Code task entrypoint for detached start / stop / restart / status, individual services (api, worker, beat, web, redis, terminal-exec), smoke, compose-full, and compose-local; always routes through local logging. |
e2e-ui.sh |
One-command UI E2E session launcher. Starts local api + web in dev-bypass mode without Azure login, or delegates to auth-on for real MSAL login, then exports headed/headless scenario environment. |
run-with-log.sh |
Lower-level wrapper that mirrors any local dev command's stdout/stderr into .logs/local/latest/*.log for warning/error review. |
VS Code dev tasks and direct terminal runs through scripts/dev/local-run.sh
write project-local logs under a single fixed location so failures are
visible from the workspace without relying on terminal scrollback:
.logs/local/
latest/ # the only place logs ever land
api.log
api.log.1 # rotated chunks (ring, see LOCAL_LOG_MAX_CHUNKS)
worker.log
beat.log
web.log
redis.log
terminal-exec.log
compose-full.log
compose-full-containers.log
<service>.launch.log # detached-launcher stdout for `local-run.sh start`
<service>.launch.pid
_archive/ # legacy session folders or `logs-clean` archives
api-<port>.lock # api start lock (flock)
There are no timestamped session folders, no latest symlink, no
.current-session marker, no .lock/ directory. One service → one file →
ring rotation.
Rules:
- one fixed log file per service:
.logs/local/latest/<service>.log; - appended across runs so a
restartdoes not lose the previous traceback; - cap each chunk at 1 MiB by default (
LOCAL_LOG_MAX_BYTES=1048576) and keep at most 5 chunks per service in a ring (LOCAL_LOG_MAX_CHUNKS=5), so each service stays under ~5 MiB on disk no matter how long you debug; - flush the first few lines immediately, then batch file flushes every 50
lines (
LOCAL_LOG_FLUSH_LINES=50) to avoid per-line filesystem pressure; - keep console output unchanged while mirroring it to files;
- set
LOCAL_LOG_CONSOLE=falsefor high-volume runs when terminal rendering is the bottleneck and file logs are enough; - replay only the newest 200 lines when starting a detached Docker Compose log
follower (
COMPOSE_LOG_TAIL=200); - ignore
.logs/in git.
Inspecting and tidying:
scripts/dev/local-run.sh logs # list .logs/local/latest/ contents with sizes
scripts/dev/local-run.sh logs-clean # move current logs into .logs/local/_archive/<ts>/
tail -f .logs/local/latest/api.log # always the right file, no symlink chasinglocal-run.sh start also performs a one-shot migration on first run: any
leftover artifacts from the retired session-folder layout (timestamped
20260515T... directories, web-debug, log-guarantee-*, .current-session,
etc.) are moved into .logs/local/_archive/<utc-ts>/ so the active directory
stays clean. Nothing is deleted.
Use .logs/local/latest/api.log first when looking for API warnings/errors,
then compare worker.log, beat.log, and web.log to verify the local
pipeline is healthy end to end.
Direct examples:
scripts/dev/local-run.sh start # detached host-mode full stack; returns immediately
scripts/dev/local-run.sh restart # stop, then detached host-mode start
scripts/dev/local-run.sh stop # stop host-mode services, compose stacks, Redis, and Azurite
scripts/dev/local-run.sh status # print host-mode service state
scripts/dev/local-run.sh api
scripts/dev/local-run.sh web
scripts/dev/local-run.sh worker
scripts/dev/local-run.sh beat
scripts/dev/local-run.sh redis
scripts/dev/local-run.sh smoke
scripts/dev/local-run.sh compose-full -- up --build
scripts/dev/local-run.sh compose-full -- up -d --build
scripts/dev/local-run.sh compose-local -- up --build
scripts/dev/local-run.sh compose-local -- up -d --buildUI E2E launcher examples:
scripts/dev/e2e-ui.sh bypass --headless
scripts/dev/e2e-ui.sh bypass --headed
scripts/dev/e2e-ui.sh login --ask-browser
scripts/dev/e2e-ui.sh bypass --headless -- npm --prefix web run e2e:dashboard
scripts/dev/e2e-ui.sh bypass --headless -- npm --prefix web run e2e:new-search
scripts/dev/e2e-ui.sh bypass --fullstack --headless -- npm --prefix web run e2e:azure-core-nt-lifecycleWhen no browser flag is supplied, e2e-ui.sh asks briefly on interactive
terminals: pressing Enter opens a visible browser, while no response falls back
to headless mode. In CI and non-interactive shells it chooses headless
automatically. Real MSAL login still requires the user to complete Microsoft
sign-in, MFA, or device-code prompts directly. Use --fullstack for scenarios
that enqueue Celery work or call the terminal exec sidecar; it starts redis,
api, worker, beat, web, and terminal-exec before running the scenario.
Host-mode API startup keeps 127.0.0.1:8085 stable because the Vite dev
server and smoke scripts expect that port. local-run.sh api takes a per-port
startup lock and checks /api/health before invoking uvicorn: if the local API
is already healthy, the command exits successfully instead of writing an opaque
Address already in use failure; if another process owns the port, the log
prints the listener details from ss or lsof.
For agent-driven or one-command local sessions, prefer local-run.sh start or
the VS Code server: start / fullstack: start task. Those commands launch
Redis, terminal-exec, api, worker, beat, and web through detached background
processes, then return immediately while service logs continue under
.logs/local/latest/. Use local-run.sh restart for a fresh server cycle and
local-run.sh status when you need readiness details without attaching to the
long-running processes.
Docker Compose logging:
- foreground
compose-full -- up --buildwritescompose-full.log; - detached
compose-full -- up -d --buildwrites the command output tocompose-full.logand starts a background follower writing container output tocompose-full-containers.log; compose-localuses the same pattern withcompose-local.logandcompose-local-containers.log.- detached followers use
docker compose logs -f --tail ${COMPOSE_LOG_TAIL:-200}so an old noisy container cannot replay an unbounded backlog into the local log pipeline. - starting a new detached compose run cleans up stale followers for the same
compose profile;
compose-full -- down|stop|rmalso stops its follower.
Running anything in Azure costs minutes. Running anything locally costs seconds. The full debug loop has three tiers; always start at the cheapest tier that can reproduce the bug.
uv run pytest -q api/tests
uv run pytest -q api/tests/test_terminal_exec.py # focusedUse for: anything that doesn't need a live HTTP server (sanitisation, auth caching, image tag dict, terminal exec contract).
When the full ~30 s suite is too slow for a tight edit-test cycle, use
test-inc.sh — it runs only the tests whose covered code
changed in your working tree (via pytest-testmon):
scripts/dev/test-inc.sh # whole suite, incremental
scripts/dev/test-inc.sh api/tests/test_foo.py # scope the coverage map
ELB_TESTMON_RESET=1 scripts/dev/test-inc.sh # rebuild the .testmondata mapThe first run builds a git-ignored .testmondata coverage map (one full run);
every later run deselects unaffected tests automatically ("N deselected /
K selected" in <1 s). testmon uses AST-level fingerprints, so comment/whitespace
edits do not trigger reruns. The wrapper clears pytest.ini's addopts because
testmon silently disables itself under -m (marker exclusion) and is
incompatible with -n auto (xdist). This is a local convenience only — CI
and the pre-push hook still run the full uv run pytest -q api/tests.
scripts/dev/local-run.sh compose-full -- up --buildFor detached compose runs, use:
scripts/dev/local-run.sh compose-full -- up -d --buildThis starts a background log follower. Check
.logs/local/latest/compose-full-containers.log for the container stream.
Then in another terminal:
curl http://127.0.0.1:18080/api/health
curl http://127.0.0.1:18080/api/health/celery # queue snapshot
curl -XPOST 'http://127.0.0.1:18080/api/health/celery/enqueue-noop?message=hi'
curl http://127.0.0.1:18080/api/health/celery/result/<id>
open http://127.0.0.1:18080/ # SPA via api proxyThe compose api binds 18080 on the host, not 8085, to avoid clashing with the workspace
api: starttask (8085) andweb: devtask (8090). If 18080 is also taken on your machine, change the host-side port indocker-compose.full.yml.
api/ is bind-mounted into the api / worker / beat containers and
uvicorn --reload watches it — code edits show up live without a rebuild.
SPA edits in web/src/ reload via vite HMR.
What this catches that a remote deploy used to:
- Celery routing trap (default queue vs. typed queues — see repo memory)
wait_redis.pyboot order across sidecars- Route registration order vs.
frontend_proxycatch-all - terminal
exec_servercontract (EXEC_TOKEN, allowlist, concurrency) - WebSocket proxy plumbing (
/api/terminal/ws) - Reverse proxy headers and SPA fallback
AUTH_DEV_BYPASSshort-circuit
What it does not catch (still requires Tier 3):
- Managed Identity /
DefaultAzureCredentialtoken acquisition - Private-endpoint networking (Storage / KV)
- Container Apps probe semantics
- ACR pull RBAC
- Real Storage Tables / blobs
When the bug only reproduces on Azure (MI, private endpoints, real Storage), do not run a full Bicep redeploy unless you actually changed sidecar structure. Use:
source /tmp/azd-env.sh # or however you populate env
scripts/dev/quick-deploy.sh api --logs # build + patch + tail logs
scripts/dev/quick-deploy.sh terminal # terminal runtime/script change
scripts/dev/quick-deploy.sh terminal --rebuild-terminal-base # force heavy terminal toolchain rebuild
scripts/dev/quick-deploy.sh frontend # only SPA changequick-deploy.sh api automatically patches worker and beat containers too
because they share the api image — leaving them on a stale tag was a real
source of confusion last week.
Terminal deploys are split into a heavy base and a thin runtime overlay. The
base is tagged from terminal/Dockerfile.base, patch_elastic_blast.py, and
merge-sharded-results.sh; normal terminal deploys reuse it and rebuild only
the runtime scripts from terminal/Dockerfile.runtime. Use
--rebuild-terminal-base when changing the installed toolchain, pinned tool
versions, or the patched elastic-blast package.
For full structural changes (env / secrets / probes / new sidecar) fall
back to postprovision.sh or az deployment group create.
- WSL DNS: WSL hosts ship a resolver at
10.255.255.254that the default Docker bridge cannot reach. The compose file works around this withdns: [8.8.8.8, 1.1.1.1]per service andbuild.network: hoston every build block. If you copy this compose to a non-WSL host you can drop both, but they are harmless if left in place. - Don't expose the terminal sidecar's ttyd in compose. It still binds
loopback inside the terminal container; the api → terminal hop in compose
uses
terminal:7682(exec_server, which honoursEXEC_HOST=0.0.0.0). ttyd browser shell is only reachable through the api WebSocket proxy. - Compose uses a fixed dev
EXEC_TOKEN. Never reuse this value in any Azure deployment. Real deploys mint a fresh GUID via BicepnewGuid(). - Compose does not wire MI / Storage / KV. Any code path that hits
azure_clientswill fail. That is intentional — those code paths must be validated in Tier 3. - Don't stop using
postprovision.sh. It is still the source of truth for sidecar layout.quick-deploy.shis for image-only iteration.