Enable multi-environment publish to shared APIM instance - #227
Open
petehauge wants to merge 3 commits into
Open
Conversation
Add an optional `environment:` block to `configuration.<env>.yaml` override
files that enables publishing dev, qa, and prod to a single shared Azure API
Management instance. Resource names are namespaced via `namePrefix` and/or
`nameSuffix`, and API paths can be scoped via `apiPathPrefix`.
Key changes:
- New `env-mapper` module — pure name/descriptor mapping (canonical <-> deployed)
- New `env-mapping-validator` — pre-flight validation with hard-block when the
environment block is present but lacks any name affix (path-only mode is
intentionally unsupported because --delete-unmatched would destroy other
environments' resources on the shared instance)
- New `policy-ref-rewriter` — regex-based rewrite of {{namedValue}} tokens,
fragment-id, and backend-id attributes in policy XML
- Publisher integration at all 5 PUT boundaries with mapDescriptor
- Normalizers extended for subscription scopes, apiRelease apiIds, MCP tool
operation ids, and named-value references
- NamedValue displayName affixing, Api path prefixing, PolicyFragment content
rewriting, and policy XML rewriting
- delete-unmatched-service namespace-scoped via toCanonicalDescriptor: a dev
publish cannot delete prod resources sharing the same APIM
- Schema v1 extended additively (no version bump) with `environment` property
- init scaffold and Copilot prompt updated
- New user guide: docs/guides/multi-environment-shared-apim.md
Closes #132
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds support for publishing multiple environments (dev/qa/prod) into a single shared Azure API Management instance by introducing an optional environment: block in override configs, applying name/path affixes at publish time, rewriting cross-resource references, and scoping --delete-unmatched to the active environment namespace.
Changes:
- Introduce env mapping (prefix/suffix + appliesTo + apiPathPrefix) with runtime validation and wire it through publish, resource publishing, and delete-unmatched.
- Add policy XML reference rewriting (
{{namedValue}},fragment-id,backend-id) and other cross-resource reference rewrites (subscription scope, apiRelease apiId, MCP tool operationIds). - Extend templates/schema/docs and add comprehensive unit test coverage for the new behavior.
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/templates/configs/config-templates.test.ts | Asserts override template includes commented environment: block and substitutions. |
| tests/unit/services/resource-publisher.env-mapping.test.ts | Validates env-mapping behavior at PUT boundaries and reference rewrites. |
| tests/unit/services/publish-service.test.ts | Adds regression coverage for incremental mode using full artifact set for env mapping / known artifacts. |
| tests/unit/services/publish-service.env-mapping.test.ts | Unit tests for env-mapping validation behavior and warnings/errors. |
| tests/unit/services/policy-ref-rewriter.test.ts | Unit tests for policy XML token/attribute rewrites with known-sets gating. |
| tests/unit/services/override-merger.test.ts | Verifies deep-merge behavior for logger credentials overrides. |
| tests/unit/services/env-mapper.test.ts | Unit tests for mapping primitives (affixing, canonicalization, descriptor mapping). |
| tests/unit/services/delete-unmatched.env-mapping.test.ts | Ensures delete-unmatched is namespace-scoped under env mapping. |
| tests/unit/models/config.environment.test.ts | Validates schema acceptance/rejection for the new environment field. |
| tests/unit/lib/config-loader.test.ts | Ensures YAML override loader parses/validates environment overrides at runtime. |
| src/templates/copilot/configure-overrides-prompt.md | Updates Copilot prompt to guide users on shared-APIM vs dedicated setups. |
| src/templates/configs/override-config.yaml | Adds commented environment: block scaffold and doc link. |
| src/services/resource-publisher.ts | Applies env mapping at PUT boundaries; adds known-artifact sets; rewrites policy refs and other cross-resource references. |
| src/services/publish-service.ts | Builds env mapping + known artifact sets (uses full artifact list in incremental mode). |
| src/services/policy-ref-rewriter.ts | New policy XML reference rewriter (regex-based with known-sets gating). |
| src/services/override-merger.ts | Tightens typing for override section keys (excludes non-section fields like environment). |
| src/services/env-mapping-validator.ts | New validator enforcing safe shared-APIM configuration and logging warnings. |
| src/services/env-mapper.ts | New mapping utilities for deployed/canonical naming and descriptor segment mapping. |
| src/services/delete-unmatched-service.ts | Scopes deletions to current env namespace and compares canonical names against local artifacts. |
| src/models/config.ts | Adds EnvironmentOverride, KnownArtifactSets, and publish-time envMapping fields. |
| src/lib/config-loader.ts | Parses environment: override block and warns on unknown keys. |
| scripts/generate-schemas.mjs | Extends schema generator to include environmentOverride definition. |
| schemas/v1/override-config.schema.json | Adds environment property + environmentOverride definition. |
| README.md | Adds guide links for environment overrides and shared-APIM publishing. |
| package.json | Adds ajv dev dependency for schema validation tests. |
| package-lock.json | Updates lockfile for ajv dependency resolution. |
| docs/guides/multi-environment-shared-apim.md | New guide documenting shared-APIM workflow, safety model, and limitations. |
| docs/guides/environment-overrides.md | Adds section linking to shared-APIM guidance and clarifying canonical naming. |
…imit Top-level APIM resource names (APIs, NamedValues, Products, Backends, etc.) are capped at 80 characters. When the environment name affix pushes a deployed name past that threshold, the ARM PUT fails mid-publish with a cryptic 400 that does not identify the affix as the cause. The validator now iterates over artifact descriptors whose type is in `appliesTo` and emits a logger.warn (not an error) when `namePrefix + canonicalName + nameSuffix` exceeds 80 characters. The warning includes the offending deployed name, the length, the resource type, and remediation guidance. It fires during `apiops publish --dry-run` too. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- env-mapper: consolidate EnvironmentOverride to a single definition in
src/models/config.ts; drop the unsafe `as EnvironmentOverride | undefined`
cast; buildEnvMapping now safely converts string[] appliesTo values to
a ReadonlySet<ResourceType> via a validated filter. The prior duplicate
interface let string[] and ResourceType[] shapes drift silently.
- delete-unmatched-service: trim trailing whitespace on the "prefix dropped
mid-life" doc comment.
- publish-service test: split `async () => { const client = ...` onto two
lines to match surrounding style.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
EMaher
approved these changes
Jul 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds multi-environment publish to a single shared Azure APIM instance via a new optional
environment:block inconfiguration.<env>.yamloverride files. Resource names are namespaced bynamePrefixand/ornameSuffix, and API paths can be scoped byapiPathPrefix.Related Issue(s)
Closes #132
What's included
New capability
environment:block in override files — configures per-environment name and path affixing so dev, qa, and prod can share one APIM--delete-unmatchedis now namespace-scoped whenenvironment:is configured — a dev publish cannot delete prod resources on the shared instance{{namedValue}}tokens,fragment-id, andbackend-idattributes are rewritten to affixed names at publish timeSafety guardrails
environment:is present but bothnamePrefixandnameSuffixare missing/empty — path-only mode is intentionally unsupported because--delete-unmatchedwould silently destroy other environments' resources[A-Za-z0-9-]only)appliesTovalidation catches unknown or non-affixable resource typesNew files
src/services/env-mapper.ts— pure name/descriptor mapping modulesrc/services/env-mapping-validator.ts— pre-flight validator with hard-block for unsafe configurationssrc/services/policy-ref-rewriter.ts— regex-based policy XML ref rewriterdocs/guides/multi-environment-shared-apim.md— new 440-line user guide with setup, safety notes, examples, and troubleshootingModified files
src/services/publish-service.ts— orchestration wiring; incremental-mode uses fulllistResourcesfor validator and known-artifact setsrc/services/resource-publisher.ts—mapDescriptorapplied at 5 PUT boundaries; 4 normalizers extended; NamedValue displayName / Api path prefix / PolicyFragment content / policy XML rewritingsrc/services/delete-unmatched-service.ts— namespace scoping viatoCanonicalDescriptorsrc/models/config.ts—EnvironmentOverride,KnownArtifactSets, extendedPublishConfigsrc/lib/config-loader.ts,src/services/override-merger.ts— schema-loader wiringschemas/v1/override-config.schema.json— additive schema extension (v1 stays v1)src/templates/configs/override-config.yaml,src/templates/copilot/configure-overrides-prompt.md— init scaffoldREADME.md,docs/guides/environment-overrides.md— cross-links to new guideTests
Design decisions (locked in with @petehauge)
nameanddisplayNameare affixed for NamedValues so{{token}}references in policies work across environments{{token}}+ targetedfragment-id/backend-idattributes (no full XML parse)pathoverride wins overapiPathPrefixDocumentation,GlobalSchema, andPolicyRestrictionare opt-in only (not inDEFAULT_APPLIES_TO)Reviews
Four internal squad reviews performed on this branch:
import.meta.urlin test)Testing