Skip to content

Add --filter flag to subset resources during publish #215

Description

@petehauge

Problem or use case

Today apiops extract accepts --filter <path> to restrict which resources are pulled out of an APIM instance, using an inclusion-list YAML (FilterConfig). The same subsetting capability does not exist on the publish side.

Users who want to publish only a subset of an artifact tree currently have to either:

  • maintain separate per-environment artifact directories, or
  • run bespoke preprocessing scripts that delete unwanted artifacts before invoking apiops publish (e.g. the prepare-apiops-artifacts.ps1 script in Azure-Samples/Apim-Samples, which physically stages and prunes a copy of the artifacts tree just to gate what publish sees).

Both approaches are error-prone, duplicate the filter concept, and don't reuse the well-tested filter machinery already in the CLI.

Proposed solution

Add a --filter <path> flag to apiops publish that:

  • Uses the same flag name as apiops extract --filter.
  • Uses the same YAML format (FilterConfig), parsed by the existing loadFilterConfig() in src/lib/config-loader.ts — including per-API and per-workspace sub-filters, wildcard patterns, and legacy *Names aliases.
  • Applies the filter to the descriptor set inside publish (filterResources() from src/services/filter-service.ts) so unlisted resources are not PUT.
  • Reuses the same case-insensitive, wildcard, API-revision, and parent→child inheritance semantics that extract already has, so a single filter file works for both directions.

Transitive dependency inclusion

To stay consistent with apiops extract, publish must also expand the filter set to include transitive dependencies by default, with the same opt-out flag:

  • Default (transitive on): after applying the filter, scan the on-disk artifacts of the matched resources for references to other resources and add those to the publish set. This mirrors extract's default behavior and prevents a common failure mode: publishing an API that references a NamedValue, Backend, PolicyFragment, or VersionSet that isn't published, which succeeds at PUT time but fails at runtime.
  • --no-transitive: same flag name as extract, disables the expansion pass. Users get exactly what the filter matches, nothing more. This is the equivalent of the PoC script's strict allowlist behavior.

References to resolve (reuse src/services/transitive-resolver.ts where possible):

  • Policy XML (in service policy, API policies, operation policies, product policies, resolver policies, policy fragments):
    • {{named-value}} → NamedValue
    • <set-backend-service backend-id="..."> → Backend
    • <include-fragment fragment-id="..."> → PolicyFragment
  • apiInformation.jsonapiVersionSetId → VersionSet

Backend-pool services[].id cross-references, productApi links, gatewayApi links, and subscription scope/apiId are common publish-time failure sources but are not covered by extract's transitive resolver today. Adding them is a natural extension of this work; call it out as a follow-up if it doesn't fit in one PR.

Transitive resolution runs after the filter and before any --commit-id intersection is finalized, so a resource pulled in transitively is always published even if it wasn't in the git diff (it's a dependency the user's diff needs).

Behavior with existing modes:

  • Default publish: only filter-matching descriptors (plus transitive deps) are published.
  • --commit-id (incremental): intersect the diff with the filter, then expand transitively.
  • --dry-run: report shows the final expanded set.
  • --delete-unmatched interaction: out of scope for this issue — track separately. Combining --filter with --delete-unmatched should fail fast with a clear error until scoped-delete semantics are designed.

Implementation notes (non-binding)

  • PublishConfig in src/models/config.ts gains an optional filter?: FilterConfig and includeTransitive: boolean (default true) to match ExtractConfig.
  • publish-command.ts mirrors extract-command.ts's filter loading (loadFilterConfig, missing-file check, error exit) and adds the same --no-transitive option.
  • publish-service.ts → determinePublishTargets calls filterResources(targetDescriptors, config.filter) after enumeration (and after computeGitDiff in incremental mode), then runs the transitive expansion pass reading artifacts from the source directory.
  • Reuse scanPolicyReferences() and related helpers from src/services/transitive-resolver.ts. Extract-side code reads references out of the fetched HTTP responses; publish-side needs an equivalent reader that walks the on-disk artifact tree via ArtifactStore. Factor the reference-scanning logic so both sides share it.
  • No changes to filter schema, filter loader, or filter-service semantics.

Out of scope (follow-ups)

  • Strict mode / audit manifest for publish filtering.
  • Interaction between --filter and --delete-unmatched (scoped-delete design).
  • Unified environment-profile file (namespace: + overrides:).
  • Pool-composition pre-flight validation.
  • Extending transitive resolution to structural JSON references (backend-pool services[].id, productApi, gatewayApi, subscription scope/apiId) if not delivered in this issue.

Acceptance criteria

  • apiops publish --filter <path> accepts the same YAML files that apiops extract --filter accepts.
  • Only resources matching the filter (plus transitive deps by default) are PUT during publish.
  • --no-transitive disables the transitive expansion pass; only exact filter matches are published.
  • Transitive expansion covers, at minimum: NamedValue, Backend, PolicyFragment refs in policy XML, and VersionSet refs via apiVersionSetId (matching extract).
  • --filter composes correctly with --commit-id (intersection, then transitive expansion) and --dry-run (report reflects final set).
  • --filter combined with --delete-unmatched fails fast with a clear error.
  • Unit tests cover: no filter (unchanged behavior), simple allowlist per resource type, wildcards, API sub-filters, incremental + filter intersection, dry-run + filter, transitive expansion on/off, --filter + --delete-unmatched rejection.
  • Docs / README publish section updated with a short "subsetting what gets published" example that reuses the extract filter file, including a note on transitive behavior and --no-transitive.

Affected command

apiops publish

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions