Skip to content

Scoped-delete semantics for --filter + --delete-unmatched #275

Open

Description

@rlarue25

Problem or use case

In #215, combining --filter with --delete-unmatched was intentionally left to fail fast until
scoped-delete semantics are designed:

Combining --filter with --delete-unmatched should fail fast with a clear error until
scoped-delete semantics are designed.

Designing and enabling that combination would allow deletion tracking for a filtered subset of the
artifact tree, rather than requiring a full-tree --delete-unmatched run just to track any
deletions at all.

Proposed solution

Is there a plan or timeline for designing scoped-delete semantics, or a place to follow along?

As a workaround, expanding the following:

 export function hasMutuallyExclusivePublishOptions(
   deleteUnmatched: boolean,
   _commitId?: string,
   hasFilter = false
 ): boolean {
-  return deleteUnmatched && hasFilter;
+  return deleteUnmatched && hasFilter && !_commitId;
 }

produced correctly-scoped create/update/delete requests when tested against a simple,
self-contained API with no shared dependencies, but this doesn't address resources with shared dependencies that would need more thoughtful deletion orchestration.

Affected command

apiops publish

Activity

  1. Alexey-Zheltov commented on Sep 9, 2026

    @Alexey-Zheltov
    Collaborator

    Thanks for the detailed write-up and for testing the workaround.

    To clarify why this fails fast rather than just being enabled: --delete-unmatched currently defines "unmatched" against the entire tree — computeDeleteActions lists every deployed resource in the service and marks for deletion anything absent from the full local artifact set. That diff has no notion of filter scope, so naively lifting the guard would make everything outside your filter look "unmatched" and get deleted. The guard is protection against a silent mass-delete, not a technical limitation of the delete path itself.

    Enabling the combination safely requires settling a few design questions first:

    1. Which set defines "unmatched"? The filter-selected artifacts only? The filter plus its transitive dependency closure (the same closure --filter uses for PUTs)? Or deployed resources whose names match the filter regardless of artifacts?
    2. Shared dependencies. Transitive expansion pulls in products, groups, loggers, named values, backends, and policy fragments that may also be referenced by resources outside the filter. Deleting one because it fell out of the filtered subset would break unrelated deployed resources — scoped delete needs reference-awareness or a "shared → never delete" rule, defined per resource type. This is exactly the case you flagged as unaddressed by the workaround.
    3. Child resources. Should scope mean "reconcile children of matched parents" (e.g., delete removed operations of a filtered API) in addition to top-level resources? Those are distinct behaviors needing distinct rules.
    4. Delete ordering. Deletes run in reverse dependency order computed from the full tree; a partial view can produce orderings that violate deployed-side dependencies the filtered set doesn't know about.
    5. Composition with existing scoping. delete-unmatched already composes with env-namespace scoping (envMapping) and commit-scoped deletes (--commit-id); filter scoping must compose with both without surprises.

    This also explains why your !_commitId workaround behaves correctly in the simple case: with --commit-id, deletions don't come from the full-tree diff at all — they come from the git diff (artifact files deleted in the commit range), which is inherently pre-scoped. That path is arguably over-guarded today, but git only tells us a file was deleted, not whether the deployed resource is still referenced by resources outside the filter — so shared dependencies remain the open problem there too.

    Input on the semantics questions above (especially 1–2) is welcome.

  2. rlarue25 commented on Sep 11, 2026

    @rlarue25
    Author

    Hi Aleksey,

    Regarding your first and second points:

    From a consistency perspective, I think the filter plus its transitive dependency closure makes the most sense for scoping deletions, since it reuses the same set that's already computed for PUTs rather than introducing a second definition of "in scope."

    For shared dependencies, I think a general reference-check rule makes more sense than defining a per-resource-type rule. It would be more complex to implement than a hardcoded per-type list, but at a high level, establishing a rule where a resource in the filter's closure is only eligible for deletion if nothing deployed outside the filtered scope still references it seems like the more logical approach.

  3. added
    staleNo activity for 30+ days — will be closed if no further activity
    on Oct 12, 2026
  4. github-actions commented on Oct 12, 2026

    @github-actions

    ⚠️ Stale issue notice — This issue has had no activity for 30 day(s).

    It will be automatically closed after 60 days of inactivity.

    To keep this issue open:

    • Leave a comment describing the current status, or
    • Remove the stale label.

    This notice was posted automatically by the stale issue workflow.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    staleNo activity for 30+ days — will be closed if no further activitytype:featureNew capability

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions