Skip to main content

lib/service-v2.sh — v2 service discovery

The discovery module that v2 commands use to resolve per-service / per-env context against the new two-project / two-repo layout.

Status: ships in v2. v1 (lib/service.sh) stays alongside until every command rewrites to v2; a post-v2 cleanup PR then removes both lib/service.sh and lib/service.ps1.


Why "discovery vs derivation vs override"

The v1 lib hardcoded rg-<env>-nrx-<APP_NAME> — the nrx literal was the customer's tenant prefix baked into open-source code. v2 deliberately avoids that pattern.

Three resolution strategies, picked per field by which one is reliable:

StrategyWhenExamples
DerivationThe value is fully determined by other values noclickops already knows (repo name, env) — no Azure call neededderive_containerapp_nameca-<repo-prefix-lc>-<svc>
DiscoveryThe value lives in Azure and noclickops queries for it (best-effort; tolerates the user not having access)discover_containerapp (queries az containerapp list), discover_pipelines (queries az pipelines list)
OverrideThe user passes a value explicitly (CLI flag → env var → function param)SVC_APP_NAME_OVERRIDE, SVC_RG_OVERRIDE, NOCLICKOPS_IAC_PROJECT, NOCLICKOPS_IAC_REPO

Functions that need a value typically try in this order:

  1. Override (immediate return — no Azure call).
  2. Discovery (one or more az calls; tolerates failure).
  3. Fall back to next strategy or die with a clear "set <env-var> to override" message.

No customer-tenant string (e.g. nrx) appears anywhere in the v2 module. All such strings are either discovered from Azure at runtime or read from the engineer-owned IaC variable files.


Public API

FunctionPurposeSide effect / output
read_service_config <svc> <env>Read services/<svc>/config.<env>.yaml from target repoExports SVC_CFG_<KEY>=<value>; sets SVC_CFG__LOADED=1. Dies on missing file.
read_iac_variables <env>Read common.yaml + <env>.yaml from IaC/platform-infrastructure/environments/<TEAM>/<repo>/infrastructure/.pipelines/variables/ via ADO RESTExports IAC_<KEY>=<value>; sets IAC__LOADED=1. Dies on REST failure or missing file.
discover_iac_projectPick the IaC project nameEchoes the name. Precedence: $NOCLICKOPS_IAC_PROJECT$IAC_IAC_PROJECT (from common.yaml) → IaC.
discover_pipelines <svc>Find all five pipelines (FrontendPlatform + IaC) for a serviceEchoes 5 lines <role>=<id> — empty value when not found.
derive_containerapp_name <svc>Pure derivation — no az callsEchoes ca-<repo-prefix-lc>-<svc>.
discover_containerapp <svc>Look up the live container app (override → IAC common RG → subscription-wide)Echoes 3 lines name=/resource_group=/fqdn=. Dies if none of the strategies hit.
public_url_for <svc> <env>URL when ENABLE_PUBLIC_ENDPOINT: "true"Echoes <svc>.<IAC_DNS_ZONE_NAME> or empty when not public. Dies if prereqs not loaded.
is_first_time_deploy <svc>Predicate — true when IaC <repo>-<svc>-deploy-test has zero prior succeeded runsExit code only (no stdout). Used by bin/deploy.sh to pick subsequent vs first-time chain.
trigger_pipeline <project> <name> [param=value ...]Wraps az pipelines run against refs/heads/mainEchoes the new run id. Dies on az failure.
watch_run <project> <run-id> [--timeout-min N]Polls the run until terminalLive overwrite-in-place ▶ in-progress (Xs)… (tty) or dots (non-tty), then a summary line (succeeded (Xm Ys) / failed (...) / timed out after Nm). On failure, calls report_pipeline_failure to print the actionable block. Exit 0 on success, 1 otherwise.
report_pipeline_failure <project> <run-id>Fetch the failed run's timeline; print step + error + actionable hintPrints the formatted failure block to stderr. Falls back to "Full log: URL" if the timeline fetch fails. Action table covers ARM "active deployment" conflict, ContainerAppInvalidName, AuthorizationFailed, ResourceGroupNotFound, Trivy CRITICAL.
report_pr_merge_failure <project> <pr-id>Query the failed PR + print actionable hintPatterns covered: draft mode, abandoned, merge conflicts, ADO hasn't computed merge yet. Falls back to "see PR URL" if the REST call fails.
report_rest_failure <verb> <url> [<status>] [<body>]Pretty-print an ADO REST failure with an actionable hintStatus-aware: 401 (refresh az login), 403 (no read access), 404 (PR-B may not be merged), 5xx (transient), 0 (network). Reads NCO_ADO_REST_LAST_STATUS if status arg omitted.
find_pr_in_project <project> <repo> <source-branch>Cross-project PR lookup by source branchEchoes the PR id or empty. Used by add-service to find both PR-A (source repo) and PR-B (IaC).
merge_pr_in_project <pr-id> <project> [<repo>]Self-approve + squash-complete + pollEchoes summary. Exit 0 on completed, 1 on abandoned / update failure / timeout. Works cross-project (each az call passes explicit --organization + --project).

Requires read_service_config to run before public_url_for; requires read_iac_variables before discover_containerapp and public_url_for. Test-only env vars: NCO_WATCH_INTERVAL (poll interval seconds; 0 = no sleep), NCO_WATCH_TIMEOUT_MIN (watch_run timeout), NCO_PR_B_TIMEOUT_MIN (add-service PR-B poll timeout, default 5 min).


Globals exported

After read_service_config <svc> <env>:

  • SVC_CFG_<KEY> — one per key in the service config file (e.g. SVC_CFG_SERVICE_PORT, SVC_CFG_ENABLE_PUBLIC_ENDPOINT, SVC_CFG_OKTA_ISSUER).
  • SVC_CFG__LOADED=1

After read_iac_variables <env>:

  • IAC_<KEY> — one per key across both common.yaml and the per-env file. Common keys: IAC_APP_NAME, IAC_APPLICATION_NAME, IAC_CONTAINER_REGISTRY_NAME, IAC_REPO_NAME, IAC_TEAM_NAME, IAC_IAC_PROJECT, IAC_IAC_REPO, IAC_ENVIRONMENT, IAC_SUBSCRIPTION_ID, IAC_COMMON_RESOURCE_GROUP_NAME, IAC_DNS_ZONE_NAME, IAC_KEY_VAULT_NAME.
  • IAC__LOADED=1

The IAC_IAC_PROJECT and IAC_IAC_REPO double-prefix is intentional — the YAML keys are IAC_PROJECT / IAC_REPO, and the auto-prefix adds IAC_ on top. Functional, just ugly.


Test shims

Both Azure-facing calls go through wrappers that tests override:

_nco_az ... # wraps `az`. Set NCO_AZ_OVERRIDE=<stub-script> to redirect.
_nco_ado_rest_get URL # wraps the REST GET. Set NCO_ADO_REST_OVERRIDE=<stub-script>.

Test fixtures (in tests/_fixtures.sh):

  • make_v2_source_repo [origin] — fake source repo with services/ + .pipelines/add-service.yaml.
  • make_v2_service <repo> <svc> [public] — adds a service folder with config + Dockerfile.
  • make_v2_iac_repo <team> <repo-name> — fake IaC repo with the variables YAMLs.
  • v2_stub_ado_rest_path — emits a stub script that maps ?path= URLs to files inside $NCO_TEST_IAC_ROOT.
  • v2_stub_az_path — emits a stub script that dispatches az ... to fixture files in $NCO_TEST_AZ_FIXTURES keyed by subcommand + --project / --resource-group / --subscription.

See tests/test-PLAN-A-service-discovery.sh for the canonical usage pattern.


End-to-end smoke (against a real target repo)

This isn't a CI test — it requires az login + access to a real new-layout repo and the IaC project. Run it manually after any change that could affect live discovery.

cd /path/to/your/source/repo # e.g. ABC100001-myservice
. /path/to/noclickops/lib/paths.sh
. /path/to/noclickops/lib/service-v2.sh

# 1. Read both config layers
read_service_config frontend test
read_iac_variables test

# 2. Inspect the loaded state
env | grep '^SVC_CFG_' | sort
env | grep '^IAC_' | sort

# 3. Discover pipelines (1 line per role, empty values OK for not-yet-created)
discover_pipelines frontend

# 4. Derive + (best-effort) discover the container app
derive_containerapp_name frontend
discover_containerapp frontend # may die if the user lacks Reader on the sub

# 5. Compute the public URL (empty if ENABLE_PUBLIC_ENDPOINT != "true")
public_url_for frontend test

What "good" looks like:

  • All five pipeline roles return non-empty IDs (matches az pipelines list).
  • derive_containerapp_name matches the actual deployed container app name (e.g. ca-<repo-prefix>-frontend).
  • discover_containerapp returns the live FQDN (only works if user has Reader on the deploy sub).
  • public_url_for frontend test returns frontend.<dns-zone> for public services.

v2 consumers (commands sourcing this module)

CommandWhat it uses
bin/info.shread_service_config + read_iac_variables + discover_containerapp (degrades on failure); public_url_for
bin/deploy.shread_service_config + read_iac_variables + discover_pipelines + is_first_time_deploy + trigger_pipeline + watch_run + derive_containerapp_name + public_url_for
bin/logs.shread_iac_variables + discover_containerapp (gates on failure)
bin/shell.shread_iac_variables + discover_containerapp (gates on failure)
bin/add-service.shdiscover_iac_project + trigger_pipeline + watch_run + find_pr_in_project + merge_pr_in_project (two-PR auto-merge)