Skip to content

fix: complete transitive dependencies for filtered extract and publish - #335

Draft
Chris Dayne (cdayne) wants to merge 23 commits into
Azure:mainfrom
cdayne:fix/filtering
Draft

Chris Dayne (cdayne) wants to merge 23 commits into
Azure:mainfrom
cdayne:fix/filtering

Conversation

@cdayne

Copy link
Copy Markdown

Summary

Makes filtered extract and publish bring in everything the selected resources depend on, so a filtered extract can be published to a new instance, and makes --remove-stale clean up dependencies nothing uses any more. It also removes the false "not found" errors that made filtered extracts on main exit with code 1, and adds wildcard API and workspace sub-filters.

Changes

Dependencies pulled in by a filtered extract and publish

  • Loggers: the logger a diagnostic sends to (in the diagnostic's own workspace, since APIM returns a workspace diagnostic's loggerId without the workspace), and loggers named by <log-to-eventhub logger-id>.
  • Tags and groups: the tags an API or product links to, and the groups a product links to. Links to a tag or group the filter excludes by name ('!name') are not extracted. Built-in groups are never pulled in. A product's link to administrators, which APIM adds to every product, is published even when the filter narrows groups, unless the filter excludes administrators by name.
  • Schemas named by validate-content's <content schema-id>, and backends named by <llm-content-safety backend-id> and the semantic cache lookups' embeddings-backend-id, as well as <set-backend-service backend-id>.
  • Named values used in logger and backend credentials, and {{references}} matched as APIM matches them: by display name first, ignoring case, then by resource name, on publish as well as extract.
  • A resource named through a named value (backend-id="{{backend-name}}", logger-id, schema-id): the named value and the resource its value names. Publish reads the value with --overrides applied, so each environment gets its own resource, and follows every policy and fragment in the source that uses the named value this way, even one the filter leaves out, because APIM checks every policy on the target when a named value changes. <include-fragment fragment-id="{{…}}"> is not followed: APIM doesn't substitute named values there.
  • Order: publish creates the backend, logger or schema before the named value that names it, and publishes policy fragments after it, because APIM rejects a named value change that would leave a policy pointing at a resource that doesn't exist.

References that aren't references

Filters

--remove-stale

  • Also removes a pulled-in dependency that the selection referenced before this extract, that this extract didn't write again, and that nothing left in the output references. A dependency another filtered extract into the same output still uses is kept, and if any artifact can't be checked, nothing is removed.

Docs

Refactor

  • Shared helpers for dependencies, links and ARM IDs. ARM ID segments are read only below the service name, so a resource group or service named products, loggers and so on is no longer misread.

Behaviour changes users may notice

  • Filtered extracts that exited with code 1 on main because of commented-out references, Liquid output or {{display name}} references now exit with code 0.
  • Filtered extract and publish bring in more resources (tags, groups, loggers, schemas, and resources named through named values). Use '!name' to keep one out.
  • Publish may create a backend, logger or schema that only an API outside the filter uses, when a published named value names it.
  • --remove-stale can now delete pulled-in dependencies, not only artifacts in the filter's scope.

Validation

  • npm test: all pass except the 2 environment-related package-build integration tests that also fail on main. npm run lint and npm run build are clean, and every commit passes on its own.
  • Integration runs on 2026-11-04 and 2026-11-05 against StandardV2 APIM source and target instances in the public cloud, with main as the baseline:
    • Filtered extract: 12 cases, covering single APIs, backend credentials and pools, exclusions, product links, wildcard sub-filters, pattern-keyed workspaces and a missing dependency. All pass. On main the first case exits with code 1 and four false errors.
    • Filtered publish: 8 cases, including publishing to an empty target, links, display-name references, incremental publish and tag descriptions to another instance. All pass.
    • --remove-stale: 7 cases. All pass.
    • Named values that name a resource: an override naming another backend, an incremental publish where only the named value changed, a fragment using it, a policy outside the filter, and the same through schema-id with schemas: []. All pass. An exclusion by name is respected; APIM then rejects the change, as expected.
    • Liquid: a named value used only in a Liquid body is extracted with no errors, publishes to a target without it, and is removed by --remove-stale once unused.
    • APIM behaviour confirmed: display names match ignoring case, administrators is linked to every product (workspace products included), and a workspace product can't link to a custom service-level group.

Not verified

  • The policy shape the Azure portal writes when you pick a backend from its list.
  • The generated CI extract pipelines with a filter.

Related Issue(s)

Fixes #315
Fixes #331
Fixes #277
Fixes #276
Fixes #316
Fixes #307
Refs #305 (closed by its reporter before this was fixed)

…nsitively

Filtered extract and publish only followed named value, backend and
policy fragment references in policies, version sets and backend pools.
The filtering guide also promised an API's products and tags. Selecting
an API therefore left out resources it needs: the logger its diagnostics
send data to, the tags it is linked to, and others.

Extract and publish now also follow:
- a diagnostic's loggerId (service-level and API diagnostics), to the
  logger in the diagnostic's own scope, since APIM leaves the workspace
  out of a workspace diagnostic's loggerId;
- named values in logger and backend credentials;
- an API's tag links to the tag, and a product's group and tag links
  to the group or tag (a link recorded at service scope targets the
  service-level resource);
- policy references to loggers (log-to-eventhub logger-id), global
  schemas (validate-content's content schema-id) and backends named by
  llm-content-safety backend-id or embeddings-backend-id on the semantic
  cache lookup policies.

Policy references are matched with the attribute anywhere on the element
(attribute by attribute, so a '>' in an earlier value does not hide it)
and in either quote, so APIM's generated
<set-backend-service id="apim-generated-policy" backend-id="..."> is
found; policy expressions (@...) and {{...}} placeholders are not treated
as resource names.

Extraction passes API diagnostics and tag links, and the product links it
wrote, to the dependency scan. In a workspace, tag links are looked up on
every workspace tag the filter does not exclude by name, when transitive
resolution is on and the filter narrows tags, so tags linked to the
extracted APIs and products are found and pulled in.

Each service-level dependency is fetched once across the service-level
pass and every workspace, including one that could not be fetched, so a
missing group is not retried and counted again. A workspace without a
sub-filter is extracted whole and still gets no dependency pass: APIM
doesn't let a workspace product link to a custom service-level group.

Policies reference a named value by its display name, as APIM resolves
them. On publish, a {{reference}} now resolves to the named value artifact
with that display name, falling back to the resource name, so a named
value whose display name differs from its name is published with the
policy. Extract already resolved display names; it now matches them
ignoring case, as publish does.

A resource the filter excludes by name ('!name') is never pulled in, and
links to it are not extracted: an API's or a product's link to an excluded
tag, or a product's link to an excluded group. An exclusion names the
resource, so a named value is checked again once its display name is
resolved. --remove-stale still treats an API's tag links as the API's, so
a link to an excluded tag left by an earlier extract is removed. Built-in
groups (administrators, developers, guests) at service level are not
pulled in either, since every APIM instance has them; a workspace group
with one of those names is a custom group and is. The list is shared with
delete-unmatched. On publish, a link to a target in the publish set is
published even when the filter leaves its type out (for example
tags: []); links to a target excluded by name are not, and links to
built-in groups follow the filter as before.

The products, gateways and subscriptions an API is linked to are still not
pulled in; the filtering guide now says so and lists what is.

Fixes Azure#315
Fixes Azure#331
Fixes Azure#277
The dependency scan read policy XML as plain text, so a commented-out
<set-backend-service backend-id="..."> or {{name}} was followed, and the
{{ ... }} output expressions in a <set-body template="liquid"> body were
fetched as named values. Each missing "dependency" counted as an
extraction error, so extract exited with 1.

XML comments, and the bodies of Liquid set-body templates, are now
removed before scanning. Only closed comments are removed, so a stray
'<!--' in a policy expression cannot hide the rest of the policy. Named
values elsewhere in the policy are still followed.

Refs Azure#305 (closed by its reporter before this was fixed)
Fixes Azure#276
A nested API filter entry applied only to the API it named, so excluding
operations or schemas from every API meant one entry per API. The key of
a nested entry can now be a wildcard pattern: it selects the matching
APIs, like a plain pattern, and its sub-filter applies to each of them:

  apis:
    - '*':
        operations: []
        schemas: []

An entry keyed by the API's name takes precedence over a pattern. One
lookup (findApiSubFilter) is now used by filtering, the specification
operation filter and API publishing.

API sub-filters also accept tagDescriptions, to choose an API's tag
descriptions by name (as stored under apis/<api>/tagDescriptions/). The
rest of Azure#307 is already handled: tag description artifacts keep the
source instance's tag ID, but publish drops that read-only property, so
they publish to another instance (confirmed by the filtering integration
tests).

Fixes Azure#316
Fixes Azure#307
…code

- Remove resolveTransitiveDependencies and addToFilter: nothing in src
  used them (only their tests), and they repeated the dependency rules in
  a filter-shaped form.
- readProductLinkDependencies reads a product's group and tag links once
  for publish scanning, service-level extraction and workspace extraction.
- src/lib/arm-id.ts replaces six helpers that each read a name or scope
  from an ARM ID (in transitive-resolver, resource-publisher,
  product-publisher and workspace-link). A malformed percent-escape now
  keeps the raw segment with a warning everywhere; extractNameFromLink
  used to throw.
  The helpers only read segments below the service
  (.../providers/Microsoft.ApiManagement/service/{name}), so a resource
  group or service named workspaces, products, loggers and so on is not
  read as that segment. extractScopeFromLinkId already matched this way;
  the other helpers matched anywhere in the ID, so this changes behaviour
  for such names. Backend pool members are read with the same helpers.
  The helpers also no longer require the name to be the last segment:
  the product name in a subscription scope (getScopedProductName) and a
  pool member's backend (when publish rewrites pool member IDs for the
  target instance) used to need the ID to end with /products/{name} or
  /backends/{name}.
- listWorkspaceTagLinks lists a workspace's tag links (apiLinks,
  productLinks) for both extractWorkspaceApiTags and
  extractWorkspaceProductTags.
- collectPolicies and apiDependencyResources build the policy keys and
  the API children scanned for dependencies on both the service-level and
  workspace extract paths.
A nested workspaces entry keyed by a pattern selected the matching
workspaces, but its sub-filter was looked up by exact name only, so it
never applied: those workspaces were extracted and published with no
filter at all.

Workspace sub-filters now use the same lookup as API sub-filters: the
entry keyed by the workspace's name, otherwise the first pattern that
matches. The check that decides what --remove-stale manages, and which
of an API's children a filtered publish includes, uses the same lookup,
so it follows a pattern-keyed sub-filter too. A sub-filter attached to
an exclusion ('!name') is ignored with a warning, for APIs and
workspaces, instead of being kept where it could never apply.
…stale

A filtered extract pulls in the resources its selection depends on
(tags, groups, loggers, named values and so on) even when the filter
leaves their type out. --remove-stale only removed artifacts in the
filter's scope, so once nothing referenced such a dependency any more
its artifact stayed, and the next publish recreated it. The other
commits here pull in many more types (loggers, tags, groups, the
backends policies name through named values), so without this every one
of them would stay in the repository for good once nothing used it.

With --remove-stale, a pulled-in artifact is now removed when the
selection referenced it before this extract (directly or through another
pulled-in artifact), this extract did not write it again, and no
artifact left in the output references it. A dependency another filtered
extract into the same output still uses is kept, as is an out-of-scope
artifact the selection never referenced. When there is a candidate, the
rest of the output is scanned, five artifacts at a time as the extract
reads them, until every candidate is known to be needed; if an artifact
can't be scanned, nothing is removed. scanArtifactReferences now skips
artifact types it can't find references in.

The reference resolver publish uses to match {{displayName}} references
to named value artifacts moves to the transitive resolver so both can
use it.
A policy can name its backend through a named value:
<set-backend-service backend-id="{{backend-name}}" />. Filtered extract
and publish pulled in the named value but not the backend its value
names, so publishing to a new instance failed: APIM refuses the policy
with "Backend with id 'function-backend' could not be found".

When a policy attribute that names a resource (backend-id, logger-id,
schema-id and the others the scan knows) is set to one named value
reference and nothing else, extract, publish and --remove-stale now also
follow the named value's value to the resource it names, in the same
scope. The named value is matched as policies reference it: by display
name, falling back to the resource name. Extract reads the value once
the named value is extracted, or from the selection when the filter
selects it; --remove-stale reads the named value artifact, and publish
reads it with --overrides applied, so each environment gets the resource
its own value names. Each named value reference is read once, however
many policies use it. On publish, a published named value also brings
the resource its value names whenever any policy or fragment in the
source uses it that way, even one the filter leaves out: APIM checks
every policy on the target that uses a named value when its value
changes, and rejects the change while the resource is missing. This
covers an incremental publish where only the named value changed. The
policies are read once, on the first named value in the publish set. A
secret or Key Vault named value's value isn't known, and a value that is
a policy expression or another reference names nothing here, so nothing
more is pulled in for those. On publish, a named value the filter
excludes by name isn't published, so neither is what it names.

APIM substitutes named values in backend-id, logger-id and schema-id,
but not in include-fragment's fragment-id, where it looks for a fragment
with the literal ID and rejects the policy, so fragment-id isn't
followed.

Found by the filtering integration tests.

Refs Azure#331
…groups

APIM links the built-in administrators group to every product, including
workspace products. Publish removes the links APIM adds to a new product
and then publishes the extracted ones, which follow the groups filter. A
filtered publish that narrows groups therefore warned "Skipping
ProductGroup association ...administrators" for every product, and left a
new product without the link APIM itself would have added.

The link to administrators is now published whatever the groups filter
says, unless the filter excludes administrators by name. Links to the
other built-in groups (developers, guests) still follow the filter.

Found by the filtering integration tests.
When a policy names its backend through a named value
(backend-id="{{backend-name}}") and the named value's value changes,
publish now pulls in the backend the new value names, but it published
all named values before any backend. APIM checks the policies that use a
named value when the named value changes, so on a target that already
has such a policy it rejected the update ("set-backend-service: An
expression in a property is valid, but its value breaks policies") and
publish exited 1. It only worked when the policy was created later in
the same run.

A named value that a policy or fragment in the source uses as the whole
value of a reference attribute (backend-id, logger-id, schema-id), and
whose value, with --overrides applied, names a backend, logger or global
schema of that kind published in the same run, now goes after those
resources, pool backends included. Every policy in the source counts,
not only the ones the filter selects, since APIM checks every policy on
the target that uses the named value; this only changes the order, not
what is published. Policy fragments then go after it, since a fragment
can use it and APIM rejects a policy that uses a missing named value.
One that a backend or logger in the same run uses in its credentials
stays first, since that resource needs it. Every other named value still
goes first, since backend and logger credentials can use them. The
policies are read only when a named value's value matches the name of
such a resource in the run.

Found by the filtering integration tests (an override naming another
backend, and an incremental publish where only the named value changed).
The dependency scan skipped every {{ ... }} inside a <set-body
template="liquid"> body, so that Liquid output expressions such as
{{body.envelope.body.fault.faultcode}} weren't fetched as named values
(Azure#276). But APIM does substitute a named value there when one has that
name: the integration tests got x=[function-backend] back from
x=[{{demo-backend-id}}], and APIM rejects a missing {{name}} in a Liquid
body when the policy is saved. A named value used only inside a Liquid
body was therefore no longer pulled in, and publishing to a new instance
failed with "Cannot find a property".

Inside a Liquid body, a {{name}} made only of the characters a named
value's name can have (letters, digits, '.', '-' and '_') is now
followed when a named value with that display or resource name exists.
Every other {{ ... }} there, with spaces, a filter or no such named
value (like {{body.id}}), is left to Liquid and skipped without an
error, so Azure#276 stays fixed. Extract checks the names against the named
values it lists, once per service or workspace extracted; publish and
--remove-stale follow only those that resolve to an artifact.

Extract's named value lookup now knows resource names as well as display
names (display names first, as in APIM). A {{FOO}} that matches no
display name used to keep its own spelling; it now takes the resource
name's (foo), so the artifact is written under that name and a '!foo'
exclusion is checked against it.

Found by the filtering integration tests.

Refs Azure#276
Copilot AI balanced review requested due to automatic review settings November 5, 2026 07:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unreported discovery and dependency-resolution failures can allow stale cleanup to delete resources that are still required.

7 open issues
What changed in this PR

This PR expands dependency resolution for filtered APIM extraction and publishing, aiming to make selected artifacts deployable to a new instance. It also extends stale-dependency cleanup and adds wildcard sub-filters.

Changes:

  • Follows additional resource references, including named-value indirection, while ignoring XML comments and unrelated Liquid expressions.
  • Adds API/workspace wildcard sub-filters, tag-description filtering, and dependency-aware publishing behavior.
  • Adds stale-dependency cleanup, shared helpers, regression tests, and updated documentation.
File Description
tests/​unit/​services/​workspace-extractor.test.ts Tests workspace dependencies and wildcard filters.
tests/​unit/​services/​transitive-resolver.test.ts Tests expanded reference detection and resolution.
tests/​unit/​services/​transitive-extractor.test.ts Tests named-value lookup, exclusions, and Liquid references.
tests/​unit/​services/​stale-dependency-finder.test.ts Tests stale candidates and shared-dependency retention.
tests/​unit/​services/​resource-publisher.test.ts Tests tag-link eligibility for pulled-in dependencies.
tests/​unit/​services/​publish-service.test.ts Tests dependency expansion, overrides, and publishing order.
tests/​unit/​services/​product-publisher.test.ts Tests group/tag links and administrator handling.
tests/​unit/​services/​filter-service.test.ts Tests wildcard filters, exclusions, and cleanup scope.
tests/​unit/​services/​extract-service.test.ts Tests dependency inclusion and exclusion behavior.
tests/​unit/​services/​api-publisher.test.ts Updates child-publisher call expectations.
tests/​unit/​services/​api-product-extractor.test.ts Tests product-link exclusions across scopes.
tests/​unit/​lib/​workspace-link.test.ts Tests shared workspace tag-link discovery.
tests/​unit/​lib/​config-loader.test.ts Tests new sub-filter syntax and exclusion warnings.
tests/​unit/​lib/​arm-id.test.ts Tests ARM resource-name and scope parsing.
tests/​unit/​cli/​extract-command.test.ts Tests stale tag and link cleanup.
src/​services/​workspace-extractor.ts Expands workspace dependencies and tag-link discovery.
src/​services/​transitive-resolver.ts Centralizes reference scanning and artifact resolution.
src/​services/​transitive-extractor.ts Resolves named values, Liquid candidates, and exclusions.
src/​services/​stale-dependency-finder.ts Identifies unused previously referenced dependencies.
src/​services/​resource-publisher.ts Updates link eligibility and ARM normalization.
src/​services/​publish-service.ts Expands publish targets and adjusts dependency ordering.
src/​services/​product-publisher.ts Uses shared eligibility and ARM helpers.
src/​services/​product-extractor.ts Filters excluded links and shares tag-link discovery.
src/​services/​filter-service.ts Adds wildcard sub-filters and stale-scope rules.
src/​services/​extract-service.ts Collects additional dependencies and shares extraction results.
src/​services/​dry-run-reporter.ts Includes publish targets in link-eligibility checks.
src/​services/​delete-unmatched-service.ts Reuses built-in group-name detection.
src/​services/​api-publisher.ts Applies wildcard sub-filters and forwards publish targets.
src/​services/​api-extractor.ts Applies wildcard operation filters and shares link discovery.
src/​models/​resource-types.ts Adds built-in group helpers.
src/​models/​config.ts Adds the API tagDescriptions sub-filter.
src/​lib/​workspace-link.ts Shares workspace tag-link enumeration and ARM helpers.
src/​lib/​config-loader.ts Loads tag-description filters and warns on exclusion sub-filters.
src/​lib/​arm-id.ts Adds shared ARM name and scope parsing.
src/​cli/​extract-command.ts Integrates stale-dependency discovery into staged commits.
scripts/​generate-schemas.mjs Updates wildcard sub-filter descriptions.
schemas/​v1/​extractor-config.schema.json Adds tag-description filtering and wildcard descriptions.
README.md Summarizes expanded publish dependencies.
docs/​reference/​dependency-graph.md Documents publishing order and dependency inclusion.
docs/​guides/​filtering-resources.md Details filters, dependencies, exclusions, and limitations.
docs/​getting-started.md Links to expanded dependency guidance.
docs/​commands/​publish.md Updates filtered-publish dependency guidance.
docs/​commands/​extract.md Documents new filters and stale-dependency cleanup.
docs/​architecture/​overview.md Updates the extraction dependency overview.

Review effort: Balanced

Comment thread src/services/transitive-resolver.ts Resolved
Comment thread src/services/workspace-extractor.ts Resolved
Comment thread src/lib/arm-id.ts Resolved
Comment thread src/services/publish-service.ts Resolved
Comment thread src/services/publish-service.ts Resolved
Comment thread src/services/stale-dependency-finder.ts Resolved
Comment thread src/services/transitive-extractor.ts Resolved
armNameAfter matched the first segment with the collection's name, so in
/workspaces/backends/backends/member it returned the workspace name
rather than the backend. The path below the service is now read as
collection/name pairs, which also covers subscription scopes in a
workspace named products.
When the filter leaves tags out, a workspace's tag links are found by
listing every tag in it. If that listing failed, only links to the
extracted tags were written, without recording an error, so
--remove-stale could delete links that still exist. The failure now
counts as an extraction error, which stops --remove-stale from running.
Matching {{references}} by display name skipped a named value artifact
that couldn't be read, so --remove-stale could report a backend stale
that a policy left in the output still reaches through that named value.
An unreadable named value now keeps every pulled-in dependency, as an
artifact that can't be scanned already does. Publish keeps its
best-effort fallback.
A dependency the extract wrote again stopped the walk over what the
selection used to depend on, so a named value only its old version
referenced (a logger's former credential, say) was never considered and
stayed in the output. Its old version is now followed too; it is still
never a candidate itself.
…ves to

A {{reference}} was dropped when an exclusion named it, before it was
resolved, so {{two}} was left out under '!two' even when it is the
display name of named value one, which the filter keeps. A named value
reference is now resolved first and checked under the resource name it
resolves to, as publish already does.
Publish found a resource's dependencies in the source JSON, before
overrides, so an override that changes a backend's credentials from
{{dev-key}} to {{prod-key}} pulled in dev-key, and publishing to a
target without prod-key failed. References are now read from the
payload as it is published, with this environment's overrides applied.
A named value that one backend uses in its credentials and whose value
names another backend was published before both, so APIM rejected it
while a policy on the target used it as a backend name and the backend
it names didn't exist yet. The order is now the backend it names, the
named value, then the backend that uses it. When the user has to go
first too (it is what the named value names, or a member of a pool
published here), the named value stays first as before.
Copilot AI balanced review requested due to automatic review settings November 5, 2026 07:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Unresolved dependency-resolution, reference-rewriting, and deployment-ordering defects can omit required resources, silently produce incomplete extractions, or cause publishing to fail.

No open issues 🎉

6 previously missed issues

In code that hasn't changed since last review

Medium severity Build the fragment reference index from overridden payloads

src/​services/​publish-service.ts:350

This index reads original fragment JSON rather than the overridden payload. If a previously deployed fragment override introduces backend-id="{{route}}", an incremental publish changing only route omits the newly named backend and APIM rejects the update. Apply fragment overrides when building this index, as dependency expansion already does.

Medium severity Resolve logger credentials by display name before rewriting

src/​services/​publish-service.ts:373

With one.displayName = 'two', two.displayName = 'three', and logger credentials {{two}}, dependency discovery includes one, but rewriteNamedValueReferences rewrites the credential to {{three}}. With namedValues: [], the logger then references an unpublished named value and fails on an empty target. Make logger credential rewriting use the same display-name-first resolution as dependency discovery.

Medium severity Rewrite named-value resource references to deployed target names

src/​services/​publish-service.ts:404

With environment.namePrefix: 'dev-', a named value containing orders-backend pulls in a backend published as dev-orders-backend, but the named-value payload still contains orders-backend. A policy using that value as backend-id therefore points at a missing backend on a clean target. Use the discovered target type to map the named-value payload's resource reference consistently with the deployed descriptor.

Medium severity Topologically order individual dependencies before using cycle fallback

src/​services/​publish-service.ts:999

When policies use route-a to name new-a and route-b to name new-b, and new-a uses {{route-b}} in its credentials, this makes route-b early and updates it before new-b exists. APIM can reject that update against existing policies even though the chain has a valid order: new-b → route-b → new-a → route-a. Order individual dependencies topologically and reserve the fallback for actual cycles.

Medium severity Report partial extraction when the named-value index is incomplete

src/​services/​transitive-extractor.ts:175

If the named-value LIST fails, loadNamedValueNames() returns an empty or partial index, so real references found only in Liquid bodies are silently omitted and extraction can report zero errors. Preserve the index's completeness status and report a partial extraction when Liquid candidates cannot be classified. Keep the existing resource-name fallback for ordinary references.

Medium severity Do not filter named values from Liquid references by resource name

src/​services/​transitive-extractor.ts:231

If a Liquid body contains {{two}}, resource two was already selected, and resource one has display name two, this filter discards the reference before it resolves to one. Extraction then omits the required named value while reporting success. Exclude named values from the initial attempted set; claimedNamedValues already deduplicates them after canonical-name resolution.

7 resolved since last review

Review effort: Balanced

A {{reference}} was dropped when a named value with that resource name
was already extracted, before it was resolved, so {{two}} was missed
when it is the display name of named value one. Named value references
are now compared with what is extracted only after they are resolved.
A {{name}} in a Liquid body is followed only when a named value has that
name, which needs the named value list. When the list failed, every
candidate was skipped with only a warning about display names, and the
extract reported no errors, so --remove-stale could run on an output
missing a named value. The failure now counts as an extraction error
when there are Liquid candidates. Other references keep falling back
to resource names.
Logger credentials were rewritten from {{name}} to the display name of
the named value with that resource name. APIM matches a reference by
display name first, so a reference that is already another named
value's display name was pointed at a different named value, one that
transitive publish doesn't pull in. A reference is now matched as APIM
matches it, and rewritten only to its own named value's (prefixed)
display name.
Overrides can replace a policy's or fragment's XML, but publish read the
source XML to find what policies reference and which named values they
use as resource names. An override adding backend-id="{{route}}" was
missed, so the backend was not pulled in, and an incremental publish
changing only route did not bring the backend it now names. Both now
read the XML as published, with this environment's overrides applied.
When a backend using one late named value in its credentials was itself
what another late named value names, that named value was published
first, before the backend it names, even when an order existed: for
route-a naming new-a, route-b naming new-b and new-a using {{route-b}},
new-b, route-b, new-a, route-a. The late named values, the backends and
loggers using them in their credentials, and the pools those are
members of are now published in waves where each comes after what it
needs. Only the named values in a real cycle go first.
Copilot AI balanced review requested due to automatic review settings November 5, 2026 11:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Publish ordering moves named values outside actual dependency cycles ahead of their required backends, causing otherwise valid updates to fail.

5 open issues

Review effort: Balanced

Comment on lines +1042 to +1045
for (const node of [...remaining].filter((candidate) => candidate.type === ResourceType.NamedValue)) {
remaining.delete(node);
early.push(node);
}
availableDescriptors.map((descriptor) => [getResourceDescriptorKey(descriptor), descriptor])
const findAvailable = createArtifactReferenceResolver(
availableDescriptors,
(descriptor) => store.readResource(sourceDir, descriptor)
Comment on lines +1698 to +1702
const namedValues = (await store.listResources(sourceDir)).filter(
(candidate) => candidate.type === ResourceType.NamedValue && candidate.workspace === descriptor.workspace
);
for (const namedValue of namedValues) {
const nvJson = await store.readResource(sourceDir, namedValue).catch(() => undefined);
Comment on lines +252 to +256
const found = [
...findTransitiveDependencies(policies, apis, workspace, resources),
...linkedDependencies,
...liquid.dependencies,
].filter((dep) => !attempted.has(getResourceDescriptorKey(dep)));
Comment on lines +417 to +419
const name = byDisplayName.has(key) ? byDisplayName.get(key) : byName.get(key);
if (name) {
dependencies.push({ type, nameParts: [name], workspace });
@cdayne
Chris Dayne (cdayne) marked this pull request as draft November 5, 2026 23:36

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants