Skip to main content

PLAN-D — logs + shell rewrite for v2

IMPLEMENTATION RULES: Before implementing this plan, read and follow:

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.sh and bin/shell.sh now source lib/service-v2.sh. v1 SVC_* globals gone; IAC_SUBSCRIPTION_ID + discover_containerapp provide 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_subscription preflight dropped; discover_containerapp's own die is the right surface for the "no Reader" case. Kept the az account show login check (skipped when NCO_AZ_OVERRIDE is 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 az semantics for clean stdin/stdout + Ctrl-C).
  • tests/test-PLAN-009-logs.sh and tests/test-PLAN-010-shell.sh deleted. New tests/test-PLAN-D-logs-shell.sh covers 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 exec calls (echoes them as AZ_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:

  • logs exec's az containerapp logs show with the discovered container app.
  • shell exec's az containerapp exec for 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

Aspectv1v2
Lib sourcelib/service.shlib/service-v2.sh
Context resolutionresolve_service_context (SVC_*)read_iac_variables (IAC_*)
App lookupaz containerapp list in derived RG, name-contains-SVC_NAMEdiscover_containerapp (override → common RG → sub-wide)
Final execaz 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, --systemUnchanged (no UX churn within v2 itself)

bin/shell.sh

Aspectv1v2
Lib sourcelib/service.shlib/service-v2.sh
Context resolutionresolve_service_contextread_iac_variables
App lookupaz containerapp list + name-containsdiscover_containerapp
Final execaz 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, --revisionUnchanged
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_context with read_iac_variables "$env". No need for read_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 a discover_containerapp "$service" call. Parse the name= / resource_group= lines. (FQDN ignored — logs doesn't need it.) If discover_containerapp dies, the script dies with its message — no extra handling needed.
  • 1.5 Replace try_az_subscription preflight — discover_containerapp will naturally fail if the user lacks sub Reader. Keep the az account show login check (cheap, gives a clearer error than discovery's "not found").
  • 1.6 Build the final az containerapp logs show invocation using $ca_name / $ca_rg / $IAC_SUBSCRIPTION_ID. exec it as before.
  • 1.7 Update SCRIPT_AUTH + SCRIPT_DETAILS to mention v2's data sources (IaC variables via ADO REST, container app discovered via az 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.shlib/service-v2.sh source line.
  • 2.2 Keep argument parsing intact (<service> [test|prod] [--command CMD] [--container NAME] [--revision NAME]).
  • 2.3 Replace resolve_service_context with read_iac_variables "$env".
  • 2.4 Replace the az containerapp list block with discover_containerapp "$service".
  • 2.5 Drop try_az_subscription preflight; keep az account show login check.
  • 2.6 Build the az containerapp exec --name $ca_name --resource-group $ca_rg --subscription $IAC_SUBSCRIPTION_ID --command $cmd [--container ...] [--revision ...] invocation. exec it.
  • 2.7 Update SCRIPT_AUTH + SCRIPT_DETAILS for 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.sh and tests/test-PLAN-010-shell.sh (v1 tests). The v1 lib (lib/service.sh) still has its own test in tests/test-PLAN-008-info.sh — those lib assertions remain (they cover yaml_var + resolve_service_context until 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:
    • --help renders 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 show with the right --name / --resource-group / --subscription / --tail. Use NCO_AZ_OVERRIDE + a stub that prints the args it received (so we can assert what az was called with). NOTE: bin/logs.sh uses exec 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_OVERRIDE set → 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.sh stays 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, logs and shell flip from "Still v1 — fails fast with Repo-level variables missing. v2 fix in PLAN-D." to "v2 — discovers container app via az containerapp list against the IaC-declared subscription/RG; override with SVC_APP_NAME_OVERRIDE + SVC_RG_OVERRIDE."
    • Update example output if there's one.
  • 4.2 Update website/docs/contributors/lib-service-v2.md: in the "Related" section (or wherever, fit naturally), mention that bin/logs.sh + bin/shell.sh are v2 consumers of discover_containerapp.

Validation

cd website && npm run build

Build clean.

User confirms phase is complete.


Acceptance Criteria

  • bin/logs.sh sources lib/service-v2.sh
  • bin/shell.sh sources lib/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_containerapp can't find the app (no degraded mode)
  • Both commands respect SVC_APP_NAME_OVERRIDE + SVC_RG_OVERRIDE overrides
  • tests/test-PLAN-D-logs-shell.sh exists and passes
  • tests/test-PLAN-009-logs.sh + tests/test-PLAN-010-shell.sh deleted
  • tests/run-all.sh green
  • 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.md
  • website/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:

  1. read_iac_variables
  2. discover_containerapp
  3. Build an az containerapp <verb> ... command
  4. 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-exec az ..., 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 / --subscription CLI flags (use env vars; flags later if asked for).
  • Multi-revision / multi-replica selection beyond what --revision already does.
  • Log filtering (grep'ing in noclickops itself — let users pipe to grep).
  • logs --since DURATION (azure-cli does support --since for log filtering — could add as a v2.x flag pass-through if useful).
  • shell --user UID (not supported by az containerapp exec; users can su once inside).