Skip to content

feat(cli): add prune command for orphaned state-file cleanup#555

Merged
michael-richey merged 6 commits into
mainfrom
prune/pr3-command
May 12, 2026
Merged

feat(cli): add prune command for orphaned state-file cleanup#555
michael-richey merged 6 commits into
mainfrom
prune/pr3-command

Conversation

@michael-richey
Copy link
Copy Markdown
Collaborator

Summary

Third and final stacked PR — adds the user-visible datadog-sync prune command that deletes orphaned per-resource state files when source resources are removed upstream.

Stacked on: #554 (PR 2 — state helpers) → which is stacked on #553 (PR 1 — storage primitives).

Why

--resource-per-file mode (mandatory for batched managed-sync workflows) writes per-resource JSON files that are never removed when the underlying resource is deleted upstream. Over time thousands of orphaned files accumulate, slowing list operations and polluting future loads.

prune is the explicit cleanup pass.

How it works

  1. Fetches authoritative source IDs via import_resources_without_saving (read-only against source — no destination calls).
  2. Builds the expected-filename set from the in-memory source IDs (sanitization + collision skip).
  3. Lists actual filenames on disk under source/ and destination/.
  4. Computes the set difference — files on disk minus expected.
  5. Snapshot fence: re-lists once more and intersects, guarding against a concurrent sync writing a file between the import and the prune.
  6. Prompts for confirmation (skipped with --force; dry-runs with --dry-run; refuses interactive prompts under --json without --force or --dry-run).
  7. Deletes via storage.delete_many; emits per-(origin, type) NDJSON ResourceOutcome events with status="success" or "partial".

Refusals (UsageError before any API call)

  • --resources must be explicit — defaulting to all types is too dangerous for a destructive command.
  • --filters rejected — would over-prune filtered-out resources.
  • --resource-per-file required — no per-file stale problem in monolithic mode.
  • --json requires --force or --dry-run — no interactive prompts in JSON mode.

Wiring

  • Command.PRUNE in constants.py.
  • commands/prune.py with @source_auth_options, @common_options, @storage_options + --force, --dry-run.
  • New dispatch branch in run_cmd_async.
  • Configuration gains resource_per_file, prune_force, prune_dry_run fields; build_config wires them post-construction.
  • _handle_deprecated is gated off for PRUNE so legacy-file presence doesn't silently mutate resources_arg.
  • init_async adds PRUNE to source-only _validate_client and _verify_ddr_status lists; PRUNE is excluded from destination validation, destination DDR checks, and send_metrics start events.

Test plan

  • 13 new unit tests in `tests/unit/test_prune_cli.py`: CLI argparse smoke (`--help`, `--resources` required, flags accepted), handler preconditions (`--resource-per-file`, `--filters`, `--json` matrix), flow tests (dry-run skip-delete, force skip-confirm, partial-failure emit, empty-stale logging, snapshot-fence intersection).
  • Full unit suite: 472/472 passing.
  • `tox -e ruff,black` clean.

Out-of-scope (deferred)

  • Integration test with VCR cassette — requires recording against a real org. The plan calls for one in `tests/integration/test_cli/test_prune.py`; happy to add as a follow-up if requested.
  • Batch-delete optimization for S3 (`DeleteObjects` API). The `delete_many` abstraction is in place so a future PR can override it for S3 without touching callers.
  • Cross-process concurrency lock. The snapshot fence covers the common case (sync running concurrently between import and prune). Sync writing during the prune itself, or in-process import racing with prune, are out of scope.

Stacked PR context

🤖 Generated with Claude Code

@michael-richey michael-richey marked this pull request as ready for review May 11, 2026 14:30
@michael-richey michael-richey requested a review from a team as a code owner May 11, 2026 14:30
heyronhay
heyronhay previously approved these changes May 11, 2026
riyazsh
riyazsh previously approved these changes May 11, 2026
@michael-richey michael-richey force-pushed the prune/pr2-state-helpers branch from 6a46806 to d60a05f Compare May 11, 2026 19:13
Base automatically changed from prune/pr2-state-helpers to main May 11, 2026 20:42
@michael-richey michael-richey dismissed stale reviews from riyazsh and heyronhay May 11, 2026 20:42

The base branch was changed.

michael-richey and others added 6 commits May 11, 2026 16:47
Adds a new top-level `prune` command that deletes per-resource state
files for source IDs that are no longer present in the source org.
Required because --resource-per-file mode (mandatory for batched
managed-sync workflows) writes per-resource JSON files that are never
removed when the underlying resource is deleted upstream — over time
thousands of orphaned files accumulate.

How it works:
1. Fetches authoritative source IDs via import_resources_without_saving
   (no destination calls — purely a read-only ground-truth query).
2. Builds the expected-filename set from the in-memory source IDs
   (using sanitization + collision skip from BaseStorage).
3. Lists actual filenames on disk under source/ and destination/.
4. Computes the set difference — files on disk minus expected.
5. Snapshot fence: re-lists once more and intersects, guarding against
   a concurrent sync writing a file between the import and the prune.
6. Prompts for confirmation (or skips with --force / dry-runs with
   --dry-run / refuses interactive prompts under --json without
   --force or --dry-run).
7. Deletes via storage.delete_many; emits per-(origin, type) NDJSON
   ResourceOutcome events with status="success" or "partial".

Refusals (UsageError before any API call):
  - --resources required to be explicit (defaulting to all is too
    dangerous for a destructive command).
  - --filters set (would over-prune the filtered-out resources).
  - --resource-per-file absent (no per-file stale problem in
    monolithic mode).
  - --json without --force or --dry-run (interactive prompts
    incompatible with JSON mode).

Wiring:
  - Command.PRUNE in constants.py.
  - prune.py in commands/ with @source_auth_options, @common_options,
    @storage_options + --force, --dry-run.
  - run_cmd_async dispatch branch.
  - Configuration gains resource_per_file, prune_force, prune_dry_run
    fields; build_config wires them post-construction. _handle_deprecated
    is gated off for PRUNE so legacy file presence doesn't silently
    mutate resources_arg. init_async adds PRUNE to source-only
    _validate_client and _verify_ddr_status lists; PRUNE is excluded
    from destination validation, DDR checks, and start metrics.

13 new unit tests (CLI argparse + handler preconditions + flow);
full suite 472/472 green; ruff/black clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The prune command's first phase calls import_resources_without_saving
to fetch authoritative source IDs. import_resources_without_saving
shows a "getting resources" progress bar when config.show_progress_bar
is true — but for prune that's misleading: the user invoked 'prune',
not 'import', and the import is internal plumbing.

Save and restore config.show_progress_bar around the internal import
call so the user's chosen setting applies elsewhere but the import
phase is silent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverts the previous commit's save/override/restore. The internal
import phase can take minutes on large orgs, and a progress bar is
genuinely useful feedback for long operations. Pass the flag through
to the import call unchanged; users can pass --show-progress-bar=False
to silence it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sets DD_SHOW_PROGRESS_BAR=false on the runner CliRunner instance so
every CliRunner.invoke from integration and unit tests runs without
a progress bar by default. Previously the CLI default of True meant
tqdm escape sequences appeared in captured test output, making CI
logs noisy and confusing pytest's output handling.

Tests that need to verify progress-bar behavior can override via
runner.invoke env= argument.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@michael-richey michael-richey merged commit 7dff57d into main May 12, 2026
11 of 12 checks passed
@michael-richey michael-richey deleted the prune/pr3-command branch May 12, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants