Skip to content

feat: implement apiops compare command#74

Draft
EMaher wants to merge 6 commits into
mainfrom
copilot/add-apiops-compare-command
Draft

feat: implement apiops compare command#74
EMaher wants to merge 6 commits into
mainfrom
copilot/add-apiops-compare-command

Conversation

@EMaher
Copy link
Copy Markdown
Contributor

@EMaher EMaher commented May 19, 2026

Adds apiops compare — a new subcommand that compares two APIM instances via the ARM REST API and reports resource-level differences. Designed for deployment verification and environment drift investigation.

Normalization logic is ported from tests/integration/all-resource-types/Compare-ApimInstance.ps1.

New modules

  • src/lib/compare-normalizer.ts — Strips ARM envelope fields, read-only/timestamp properties, and replaces instance-specific values with stable placeholders before comparison:

    • Subscription IDs, resource groups, service names → {{sub}}, {{rg}}, {{apim-name}}
    • Key Vault URIs, App Insights/Event Hub resource names → placeholders
    • 24-char hex auto-generated IDs and GUIDs → {{auto-id}} / {{guid}}
    • Resources with auto-generated names keyed by sorted normalized content as {{auto-id-0}}, {{auto-id-1}}, … for stable cross-instance matching
    • APIM ARM paths derived from context.baseUrl (via getArmPathFromBaseUrl / splitAtProviders) — no hardcoded provider type strings
  • src/lib/compare-differ.ts — Deep recursive JSON diff with dot-notation paths; skips .properties.value on secret named values and .properties.credentials on EventHub/AppInsights loggers. Each ResourceDiff carries an explicit status field (missing | extra | different) for structured output.

  • src/services/compare-service.ts — Hierarchical orchestrator using the same dependency graph and resource-path libraries as extract and publish:

    • Top-level types derived from TIER_1_RESOURCES, TIER_2_RESOURCES, and non-child TIER_3_RESOURCES (via isChildType()) — same pattern as extract-service.ts
    • Child types per parent (API, Product, Gateway) derived from getDependencies() via getChildTypesOf() — new resource types added to the dependency graph are automatically included
    • Per-type options (exclusion lists, secret/credential skip flags) centralized in a TYPE_OPTIONS map
    • Built-in exclusions: echo-api, administrators/developers/guests, starter/unlimited, master
  • src/cli/compare-command.ts — Commander subcommand with --format text|table|json output modes and plain-text (emoji-free) console output:

    • text (default): per-type summary with field-level diff details
    • table: ASCII table with Resource (by resource ID), Status (missing, extra, different, skipped), and Notes columns
    • json: same row structure as table, machine-readable

CLI interface

apiops compare \
  --source-subscription-id <id> \
  --source-resource-group <rg> --source-service-name <name> \
  --target-subscription-id <id> \
  --target-resource-group <rg> --target-service-name <name> \
  [--format text|table|json]

All six resource-identity flags are required. --source-subscription-id and --target-subscription-id are independent — use them when the instances are in different subscriptions or the same one.

Exit codes: 0 = identical, 1 = differences found, 2 = fatal error

Supporting changes

  • src/cli/index.ts--subscription-id removed from global options; it is now a per-command required option on extract and publish (with AZURE_SUBSCRIPTION_ID env var fallback)
  • src/cli/extract-command.ts / src/cli/publish-command.ts--subscription-id added as a mandatory per-command option
  • src/models/config.ts — adds CompareConfig
  • specs/contracts/cli-commands.md — updated global options table, extract/publish option tables, and compare command documentation
  • specs/tasks.md — Phase 9 added with all compare tasks

Copilot AI and others added 6 commits May 19, 2026 11:46
Closes #21

Adds full implementation of `apiops compare` command for comparing two
Azure API Management instances via the ARM REST API.

Files added:
- src/lib/compare-normalizer.ts — normalization engine (string replacement,
  auto-ID keying, resource map building)
- src/lib/compare-differ.ts — diff engine (deep JSON diff, resource map
  comparison, secret/credential skip)
- src/services/compare-service.ts — orchestrator (hierarchical comparison:
  top-level, API children, product/gateway/workspace children)
- src/cli/compare-command.ts — CLI registration + text/JSON output
- tests/unit/lib/compare-normalizer.test.ts
- tests/unit/lib/compare-differ.test.ts
- tests/unit/services/compare-service.test.ts
- tests/unit/cli/compare-command.test.ts

Files modified:
- src/models/config.ts — adds CompareConfig
- src/cli/index.ts — registers compare command
- specs/contracts/cli-commands.md — documents compare command
- specs/tasks.md — Phase 9 with all compare tasks marked done

Agent-Logs-Url: https://github.com/Azure/apiops-cli/sessions/a77d8d45-6ce6-4ca7-a7ea-9191c35edc17

Co-authored-by: EMaher <9244742+EMaher@users.noreply.github.com>
…izer

Previously, normalizeString built the full APIM ARM path by assembling
subscriptionId + resourceGroup + serviceName with a hardcoded
'Microsoft.ApiManagement/service' provider string.

Now it uses getArmPathFromBaseUrl(context.baseUrl) to extract the path
component from the already-constructed base URL (built by buildArmBaseUrl
in cloud-config.ts), and splitAtProviders() to derive the subscription+RG
prefix. This removes the hardcoded provider type from the normalizer.

Added tests for the two new exported helpers.

Agent-Logs-Url: https://github.com/Azure/apiops-cli/sessions/60174382-313e-441b-b12f-562cf1f89617

Co-authored-by: EMaher <9244742+EMaher@users.noreply.github.com>
…ally exclusive in compare

--subscription-id (global) is for when both APIM instances are in the
same subscription. --source-subscription-id + --target-subscription-id
are for the cross-subscription case. Using both groups simultaneously is
now rejected with exit code 2.

Extracted resolveSubscriptionIds() pure helper for testability. Added 9
unit tests covering: shared sub, env-var fallback, per-side pair,
mutual-exclusion violation, single per-side flag, and missing sub ID.

Updated specs/contracts/cli-commands.md with a subscription note table.

Agent-Logs-Url: https://github.com/Azure/apiops-cli/sessions/7bdc7855-c1c2-4087-a9f8-a5ca471577ab

Co-authored-by: EMaher <9244742+EMaher@users.noreply.github.com>
… and resource-path libs

Replace hardcoded resource type lists with derivations from the same
model used by extract and publish:

- TOP_LEVEL_TYPES = TIER_1 + TIER_2 + non-child TIER_3 (mirrors extract-service)
- getChildTypesOf(parent) uses getDependencies() from dependency-graph.ts
  so newly added resource types are automatically included
- TYPE_OPTIONS map centralises per-type skip/exclude flags
- RESOURCE_TYPE_LABEL map centralises display labels with enum-name fallback
- McpServer now automatically included as an API child (was previously missing)

Agent-Logs-Url: https://github.com/Azure/apiops-cli/sessions/685cf23c-6c2a-4672-b8f8-8f7331c04b88

Co-authored-by: EMaher <9244742+EMaher@users.noreply.github.com>
… no emojis

- Remove --subscription-id from global CLI options; add it as a
  per-command required option (with AZURE_SUBSCRIPTION_ID env var
  support) to extract and publish commands
- Make --source-subscription-id and --target-subscription-id required
  options on compare (no global subscription-id fallback)
- Remove resolveSubscriptionIds() helper (no longer needed)
- Add --format text|table|json to compare command; table shows
  Resource / Status / Notes columns per resource; json mirrors the
  same row structure
- Remove all emojis from compare text output
- Add status field ('missing' | 'extra' | 'different') to ResourceDiff
  to make categorization explicit
- Update spec doc, all affected tests

Agent-Logs-Url: https://github.com/Azure/apiops-cli/sessions/5742ddd2-d902-42cb-bc38-c94c3ffe182d

Co-authored-by: EMaher <9244742+EMaher@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants