PLAN-D — logs + shell rewrite for v2
IMPLEMENTATION RULES: Before implementing this plan, read and follow:
- WORKFLOW.md — the implementation process
- PLANS.md — plan structure and best practices
Status: Completed
Goal: Rewrite bin/logs.sh and bin/shell.sh on lib/service-v2.sh. Both commands use the same v2 discovery primitives (read_iac_variables + discover_containerapp) — bundled in one plan because the rewrite is identical except for the final az invocation.
Last Updated: 2026-05-29
Completed: 2026-05-29
Completion notes
- Both
bin/logs.shandbin/shell.shnow sourcelib/service-v2.sh. v1SVC_*globals gone;IAC_SUBSCRIPTION_ID+discover_containerappprovide all context. - Both gate on discovery failure (die loudly with the override-env-var message) — appropriate since neither can function with an unknown container app.
try_az_subscriptionpreflight dropped;discover_containerapp's own die is the right surface for the "no Reader" case. Kept theaz account showlogin check (skipped whenNCO_AZ_OVERRIDEis set for tests).- Final call uses
exec "${NCO_AZ_OVERRIDE:-az}" ...so tests can intercept the final exec'd command — same UX as before for real users (exec azsemantics for clean stdin/stdout + Ctrl-C). tests/test-PLAN-009-logs.shandtests/test-PLAN-010-shell.shdeleted. Newtests/test-PLAN-D-logs-shell.shcovers both (43 assertions): help / no-args / outside-repo / invalid-env / happy path for both / flag pass-through / gating on discovery failure / override path.- Test uses a PLAN-D-specific az stub that intercepts the final exec'd
containerapp logs show/containerapp execcalls (echoes them asAZ_CALL: ...) while delegating other queries to fixture files. The stub is auto-generated + gitignored. - Total tests: 435 → 430 (net after the v1 test deletion + PLAN-D test addition).
- Branch stays
feat/v2-new-target-structure. Per CLAUDE.md PR-per-investigation rule.
Investigation: INVESTIGATE-new-target-structure.md (see § "Per-command impact → logs / shell" and § "PLAN sequence → PLAN-D")
Prerequisites: PLAN-A ships discover_containerapp + read_iac_variables.
Branch: same as PLAN-A/B/C — feat/v2-new-target-structure.
Overview
logs and shell are siblings:
logsexec'saz containerapp logs showwith the discovered container app.shellexec'saz containerapp execfor an interactive session.
Both need: the live container-app name + resource_group + subscription. v1 derived these from .pipelines/variables/ + a hardcoded rg-<env>-nrx-<APP_NAME> pattern. v2 reads IAC_SUBSCRIPTION_ID from the IaC variables and asks Azure for the actual deployed name + RG via discover_containerapp.
Both are gating (unlike info, which degrades): if discovery comes up empty, the command dies with a clear "set SVC_APP_NAME_OVERRIDE=<name> and SVC_RG_OVERRIDE=<rg> to override" message. No degraded mode — you can't stream logs from "(unavailable)".
The lib-level functions already do everything needed; this plan is two thin bin/ rewrites plus tests.
What changes per command
bin/logs.sh
| Aspect | v1 | v2 |
|---|---|---|
| Lib source | lib/service.sh | lib/service-v2.sh |
| Context resolution | resolve_service_context (SVC_*) | read_iac_variables (IAC_*) |
| App lookup | az containerapp list in derived RG, name-contains-SVC_NAME | discover_containerapp (override → common RG → sub-wide) |
| Final exec | az containerapp logs show --name <app> --resource-group <rg> --subscription <sub> --tail N [--follow] [--type system] | Same shape, but <app> / <rg> / <sub> come from discover_containerapp / IAC_* |
| Flag set | --follow/-f, --tail N, --system | Unchanged (no UX churn within v2 itself) |
bin/shell.sh
| Aspect | v1 | v2 |
|---|---|---|
| Lib source | lib/service.sh | lib/service-v2.sh |
| Context resolution | resolve_service_context | read_iac_variables |
| App lookup | az containerapp list + name-contains | discover_containerapp |
| Final exec | az containerapp exec --name <app> --resource-group <rg> --subscription <sub> --command <cmd> [--container N] [--revision N] | Same shape; <app> / <rg> / <sub> from discovery |
| Flag set | --command, --container, --revision | Unchanged |
Default cmd | /bin/sh (matches nginx:alpine base) | /bin/sh (still a reasonable default for the new sample shape) |
Both commands continue to use exec az ... as the final call so stdin/stdout wire straight to az — required for interactive shell + Ctrl-C in --follow mode.
Phase 1: Rewrite bin/logs.sh — DONE
Tasks
- 1.1 Replace
. "$_dir/../lib/service.sh"→. "$_dir/../lib/service-v2.sh". Update the docstring at the top to describe v2's data sources. - 1.2 Keep the existing argument parsing (positional
<service> [test|prod]+--follow/-f/--tail N/--system). No flag changes. - 1.3 Replace
resolve_service_contextwithread_iac_variables "$env". No need forread_service_config— logs doesn't print service config; it just needs sub/RG context. - 1.4 Replace the
az containerapp list+ name-match block with adiscover_containerapp "$service"call. Parse thename=/resource_group=lines. (FQDN ignored — logs doesn't need it.) Ifdiscover_containerappdies, the script dies with its message — no extra handling needed. - 1.5 Replace
try_az_subscriptionpreflight —discover_containerappwill naturally fail if the user lacks sub Reader. Keep theaz account showlogin check (cheap, gives a clearer error than discovery's "not found"). - 1.6 Build the final
az containerapp logs showinvocation using$ca_name/$ca_rg/$IAC_SUBSCRIPTION_ID.execit as before. - 1.7 Update
SCRIPT_AUTH+SCRIPT_DETAILSto mention v2's data sources (IaC variables via ADO REST, container app discovered viaaz containerapp list, override env vars).
Validation
bash bin/logs.sh --help # static check
bash tests/run-all.sh # no regression
User confirms phase is complete.
Phase 2: Rewrite bin/shell.sh — DONE
Same as Phase 1 but for shell. Verbatim repeat of the discovery flow — just the final exec line differs.
Tasks
- 2.1 Swap
lib/service.sh→lib/service-v2.shsource line. - 2.2 Keep argument parsing intact (
<service> [test|prod] [--command CMD] [--container NAME] [--revision NAME]). - 2.3 Replace
resolve_service_contextwithread_iac_variables "$env". - 2.4 Replace the
az containerapp listblock withdiscover_containerapp "$service". - 2.5 Drop
try_az_subscriptionpreflight; keepaz account showlogin check. - 2.6 Build the
az containerapp exec --name $ca_name --resource-group $ca_rg --subscription $IAC_SUBSCRIPTION_ID --command $cmd [--container ...] [--revision ...]invocation.execit. - 2.7 Update
SCRIPT_AUTH+SCRIPT_DETAILSfor v2.
Validation
bash bin/shell.sh --help
bash tests/run-all.sh
User confirms phase is complete.
Phase 3: Tests + delete v1 — DONE
Tasks
- 3.1 Delete
tests/test-PLAN-009-logs.shandtests/test-PLAN-010-shell.sh(v1 tests). The v1 lib (lib/service.sh) still has its own test intests/test-PLAN-008-info.sh— those lib assertions remain (they coveryaml_var+resolve_service_contextuntil PLAN-F's cleanup). - 3.2 Create
tests/test-PLAN-D-logs-shell.sh. Single file covers both commands since they share the same flow. Cover:--helprenders v2 metadata for both commands.- No args → exit 1 + usage (each command).
- Outside a git repo → clear error (each command).
- Invalid env → exit 1 + "Invalid environment" (each command).
- Logs happy path: with v2 fixtures + stubs that have a containerapp hit, the script exec's
az containerapp logs showwith the right--name/--resource-group/--subscription/--tail. UseNCO_AZ_OVERRIDE+ a stub that prints the args it received (so we can assert what az was called with). NOTE: bin/logs.sh usesexec az ...which replaces the process — the stub IS the final az call. - Shell happy path: same idea, asserting
az containerapp exec --command /bin/sh(the default). - Gating on discovery failure: stub returns empty containerapp list for both RG + sub-wide; both commands die with non-zero exit + the "set SVC_APP_NAME_OVERRIDE" message.
- Override path:
SVC_APP_NAME_OVERRIDE+SVC_RG_OVERRIDEset → no containerapp list call needed (discover_containerapp short-circuits to the override); final az invocation uses the overrides. - Flag pass-through for logs:
--follow+--tail 50+--system→ asserted in the captured az invocation. - Flag pass-through for shell:
--command "/bin/bash"+--container web→ asserted.
- 3.3 Verify
tests/test-portability.shstays green (no new tenant strings leaked).
Validation
bash tests/run-all.sh
Total passing count rises by however many the new test file adds. Zero failures.
User confirms phase is complete.
Phase 4: Docs sync — DONE
Tasks
- 4.1 Update
website/docs/getting-started.md:- In the compatibility matrix,
logsandshellflip from "Still v1 — fails fast with Repo-level variables missing. v2 fix in PLAN-D." to "v2 — discovers container app viaaz containerapp listagainst the IaC-declared subscription/RG; override withSVC_APP_NAME_OVERRIDE+SVC_RG_OVERRIDE." - Update example output if there's one.
- In the compatibility matrix,
- 4.2 Update
website/docs/contributors/lib-service-v2.md: in the "Related" section (or wherever, fit naturally), mention thatbin/logs.sh+bin/shell.share v2 consumers ofdiscover_containerapp.
Validation
cd website && npm run build
Build clean.
User confirms phase is complete.
Acceptance Criteria
-
bin/logs.shsourceslib/service-v2.sh -
bin/shell.shsourceslib/service-v2.sh - No reference to v1's
SVC_*globals in either bin file - No hardcoded
rg-<env>-nrx-...pattern anywhere new - Both commands die loudly when
discover_containerappcan't find the app (no degraded mode) - Both commands respect
SVC_APP_NAME_OVERRIDE+SVC_RG_OVERRIDEoverrides -
tests/test-PLAN-D-logs-shell.shexists and passes -
tests/test-PLAN-009-logs.sh+tests/test-PLAN-010-shell.shdeleted -
tests/run-all.shgreen - Docs reflect v2-ready status for both commands
Files to Modify
bin/logs.sh(rewrite)bin/shell.sh(rewrite)tests/test-PLAN-009-logs.sh(delete)tests/test-PLAN-010-shell.sh(delete)tests/test-PLAN-D-logs-shell.sh(new)website/docs/getting-started.mdwebsite/docs/contributors/lib-service-v2.md(small Related update)
Implementation Notes
Why one plan for two commands
logs and shell are mechanically identical at the v2 lib layer. Both:
read_iac_variablesdiscover_containerapp- Build an
az containerapp <verb> ...command exec az
The only difference is the verb (logs show vs exec) and the flag set. Splitting into two plans would duplicate ~80% of the scaffolding without adding any decision points. One plan, two trivial bin rewrites.
Why gating not degrading
info degrades on discovery failure (prints "unavailable" and exits 0) because its job is to show what we know. logs and shell can't function without an actual container app — there's nothing to stream from "(unavailable)". Failing closed with a clear "set the overrides to fix" message is the right UX.
discover_containerapp's default die behavior is exactly right for this; no extra wrapping needed (unlike info's output=$(... 2>&1) || output="" pattern).
Why the final exec az matters
Both v1 commands use exec az ... instead of az ...; exit $? so the az process REPLACES the bash process. Two reasons:
- Interactive shell:
shell's stdin/stdout need to wire directly to az exec without bash buffering. - Ctrl-C in
--follow: when the user hits Ctrl-C, the signal goes straight to az, which cleans up the streaming connection. With a non-execaz ..., bash would catch the signal first and the cleanup is messier.
v2 keeps the exec az pattern. Don't change to az ...; exit $?.
--app-name / --resource-group as CLI flags?
The investigation says info/logs/shell "require --subscription + --resource-group + --app-name overrides if discovery fails." That implies CLI flags. v2 chose env vars (SVC_APP_NAME_OVERRIDE + SVC_RG_OVERRIDE) instead because they compose better with --watch / --follow / --tail-style flags users already remember, and they're stickier across multiple commands in the same shell session.
If real users want CLI flags, add them later — they're a thin wrapper around the env vars. Out of scope for PLAN-D.
Out of scope for PLAN-D
--app-name/--resource-group/--subscriptionCLI flags (use env vars; flags later if asked for).- Multi-revision / multi-replica selection beyond what
--revisionalready does. - Log filtering (grep'ing in noclickops itself — let users pipe to
grep). logs --since DURATION(azure-cli does support--sincefor log filtering — could add as a v2.x flag pass-through if useful).shell --user UID(not supported byaz containerapp exec; users cansuonce inside).