Skip to content

Commit 0f7723b

Browse files
Add cleanup-eligible drain command (#785)
* Add cleanup-eligible drain command * Fix cleanup drain lint --------- Co-authored-by: homeboy-ci[bot] <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent 599962d commit 0f7723b

4 files changed

Lines changed: 577 additions & 5 deletions

File tree

inc/Abilities/WorkspaceAbilities.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use DataMachineCode\Workspace\RunnerWorkspacePublisher;
2222
use DataMachineCode\Workspace\Workspace;
2323
use DataMachineCode\Workspace\WorkspaceAbandonedCleanupOrchestrator;
24+
use DataMachineCode\Workspace\WorkspaceCleanupEligibleDrainOrchestrator;
2425
use DataMachineCode\Workspace\WorkspaceReader;
2526
use DataMachineCode\Workspace\WorkspaceWriter;
2627
use DataMachineCode\Support\GitRunner;
@@ -2424,6 +2425,35 @@ private function registerAbilities(): void {
24242425
)
24252426
);
24262427

2428+
AbilityRegistry::register(
2429+
'datamachine-code/workspace-worktree-cleanup-eligible-drain',
2430+
array(
2431+
'label' => 'Drain Cleanup-Eligible Worktrees',
2432+
'description' => 'Repeat the bounded cleanup-eligible apply primitive until no safe candidates remain, the pass limit is reached, or the time budget expires. Defaults to preview mode and never discards unpushed commits.',
2433+
'category' => 'datamachine-code-workspace',
2434+
'input_schema' => array(
2435+
'type' => 'object',
2436+
'properties' => array(
2437+
'apply' => array( 'type' => 'boolean' ),
2438+
'force' => array( 'type' => 'boolean' ),
2439+
'limit' => array( 'type' => 'integer' ),
2440+
'passes' => array( 'type' => 'integer' ),
2441+
'until_budget' => array( 'type' => 'string' ),
2442+
'older_than' => array( 'type' => 'string' ),
2443+
'sort' => array( 'type' => 'string' ),
2444+
'remove_timeout' => array( 'type' => 'integer' ),
2445+
'include_repaired_metadata' => array( 'type' => 'boolean' ),
2446+
'discard_unpushed' => array( 'type' => 'boolean' ),
2447+
'source' => array( 'type' => 'string' ),
2448+
),
2449+
),
2450+
'output_schema' => array( 'type' => 'object' ),
2451+
'execute_callback' => array( self::class, 'worktreeCleanupEligibleDrain' ),
2452+
'permission_callback' => fn() => PermissionHelper::can_manage(),
2453+
'meta' => array( 'show_in_rest' => false ),
2454+
)
2455+
);
2456+
24272457
AbilityRegistry::register(
24282458
'datamachine-code/workspace-cleanup-plan',
24292459
array(
@@ -4283,6 +4313,18 @@ public static function worktreeBoundedCleanupEligibleApply( array $input ): arra
42834313
return $workspace->worktree_bounded_cleanup_eligible_apply($opts);
42844314
}
42854315

4316+
/**
4317+
* Drain cleanup-eligible worktrees with repeated bounded apply passes.
4318+
*
4319+
* @param array $input Input parameters.
4320+
* @return array<string,mixed>|\WP_Error
4321+
*/
4322+
public static function worktreeCleanupEligibleDrain( array $input ): array|\WP_Error {
4323+
$orchestrator = new WorkspaceCleanupEligibleDrainOrchestrator();
4324+
4325+
return $orchestrator->run($input);
4326+
}
4327+
42864328
/**
42874329
* Build or apply a disk-pressure emergency cleanup plan.
42884330
*

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class WorkspaceCommand extends BaseCommand {
4747
'cleanup' => array( 'ability' => 'datamachine-code/workspace-worktree-cleanup' ),
4848
'cleanup-artifacts' => array( 'ability' => 'datamachine-code/workspace-worktree-cleanup-artifacts' ),
4949
'bounded-cleanup-eligible-apply' => array( 'ability' => 'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' ),
50+
'cleanup-eligible-drain' => array( 'ability' => 'datamachine-code/workspace-worktree-cleanup-eligible-drain' ),
5051
'emergency-cleanup' => array( 'ability' => 'datamachine-code/workspace-worktree-emergency-cleanup' ),
5152
'reconcile-metadata' => array( 'ability' => 'datamachine-code/workspace-worktree-reconcile-metadata' ),
5253
'active-no-signal-report' => array(
@@ -3211,7 +3212,7 @@ private function renderGitOperationResult( string $operation, array $result, arr
32113212
*
32123213
* <operation>
32133214
* : Worktree operation: add, list, remove, prune, locks, cleanup, cleanup-artifacts,
3214-
* bounded-cleanup-eligible-apply, emergency-cleanup, reconcile-metadata,
3215+
* bounded-cleanup-eligible-apply, cleanup-eligible-drain, emergency-cleanup, reconcile-metadata,
32153216
* active-no-signal-report, active-no-signal-finalized-apply,
32163217
* active-no-signal-equivalent-clean-apply,
32173218
* active-no-signal-merged-apply, active-no-signal-remote-clean-apply,
@@ -3358,7 +3359,8 @@ private function renderGitOperationResult( string $operation, array $result, arr
33583359
* [--discard-unpushed]
33593360
* : With bounded-cleanup-eligible-apply only, explicitly discard unpushed
33603361
* commits after reviewed cleanup eligibility and fresh safety probes. This
3361-
* is a data-loss mode and is not implied by --force.
3362+
* is a data-loss mode and is not implied by --force. Cleanup-eligible-drain
3363+
* refuses this option.
33623364
*
33633365
* [--older-than=<duration>]
33643366
* : Limit cleanup candidates to worktrees with lifecycle `created_at`
@@ -3414,7 +3416,8 @@ private function renderGitOperationResult( string $operation, array $result, arr
34143416
*
34153417
* [--passes=<count>]
34163418
* : For `abandoned`, maximum apply passes to run after marking eligible rows.
3417-
* Preview mode always runs a single non-destructive classification pass.
3419+
* For `cleanup-eligible-drain`, maximum bounded cleanup-eligible apply
3420+
* passes to run. Preview mode always runs one non-destructive pass.
34183421
*
34193422
* [--stage=<stage>]
34203423
* : For `abandoned`, resume from a specific orchestration stage. Supported
@@ -3427,7 +3430,7 @@ private function renderGitOperationResult( string $operation, array $result, arr
34273430
* passing the previous response's `pagination.next_offset`.
34283431
*
34293432
* [--until-budget=<duration>]
3430-
* : For `cleanup --dry-run` and `reconcile-metadata`, enforce a compact
3433+
* : For `cleanup --dry-run`, `cleanup-eligible-drain`, and `reconcile-metadata`, enforce a compact
34313434
* wall-clock budget for dry-run pages or direct-apply drains (e.g. 60s,
34323435
* 10m). Also supported by `active-no-signal-report` and the active/no-signal
34333436
* apply flows. Returns continuation
@@ -3524,6 +3527,8 @@ private function renderGitOperationResult( string $operation, array $result, arr
35243527
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --via-jobs --limit=10 --older-than=7d
35253528
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --dry-run --include-repaired-metadata --older-than=7d --limit=25
35263529
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --include-repaired-metadata --older-than=7d --limit=25
3530+
* wp datamachine-code workspace worktree cleanup-eligible-drain --limit=25 --format=json
3531+
* wp datamachine-code workspace worktree cleanup-eligible-drain --apply --limit=25 --passes=10 --until-budget=120s --format=json
35273532
*
35283533
* # Local-only detection (no GitHub API call)
35293534
* wp datamachine-code workspace worktree cleanup --skip-github
@@ -3581,7 +3586,7 @@ public function worktree( array $args, array $assoc_args ): void {
35813586
$operation = $args[0] ?? '';
35823587

35833588
if ( '' === $operation ) {
3584-
WP_CLI::error('Usage: wp datamachine-code workspace worktree <add|list|remove|prune|locks|cleanup|cleanup-artifacts|abandoned|bounded-cleanup-eligible-apply|emergency-cleanup|reconcile-metadata|backfill-origin-session|active-no-signal-report|active-no-signal-finalized-apply|active-no-signal-equivalent-clean-apply|active-no-signal-merged-apply|active-no-signal-remote-clean-apply|refresh-context|finalize|mark-cleanup-eligible> [<repo>] [<branch>] [--flags]');
3589+
WP_CLI::error('Usage: wp datamachine-code workspace worktree <add|list|remove|prune|locks|cleanup|cleanup-artifacts|abandoned|bounded-cleanup-eligible-apply|cleanup-eligible-drain|emergency-cleanup|reconcile-metadata|backfill-origin-session|active-no-signal-report|active-no-signal-finalized-apply|active-no-signal-equivalent-clean-apply|active-no-signal-merged-apply|active-no-signal-remote-clean-apply|refresh-context|finalize|mark-cleanup-eligible> [<repo>] [<branch>] [--flags]');
35853590
return;
35863591
}
35873592

@@ -3898,6 +3903,24 @@ public function worktree( array $args, array $assoc_args ): void {
38983903
$input['remove_timeout'] = (int) $assoc_args['remove-timeout'];
38993904
}
39003905
break;
3906+
3907+
case 'cleanup-eligible-drain':
3908+
$input['apply'] = ! empty($assoc_args['apply']);
3909+
$input['force'] = ! empty($assoc_args['force']);
3910+
$input['discard_unpushed'] = ! empty($assoc_args['discard-unpushed']);
3911+
$input['include_repaired_metadata'] = ! empty($assoc_args['include-repaired-metadata']);
3912+
$input['source'] = self::CLEANUP_CLI_SOURCE;
3913+
foreach ( array( 'limit', 'passes', 'remove-timeout' ) as $key ) {
3914+
if ( isset($assoc_args[ $key ]) ) {
3915+
$input[ str_replace('-', '_', $key) ] = (int) $assoc_args[ $key ];
3916+
}
3917+
}
3918+
foreach ( array( 'older-than', 'sort', 'until-budget' ) as $key ) {
3919+
if ( isset($assoc_args[ $key ]) && '' !== trim( (string) $assoc_args[ $key ]) ) {
3920+
$input[ str_replace('-', '_', $key) ] = trim( (string) $assoc_args[ $key ]);
3921+
}
3922+
}
3923+
break;
39013924
}
39023925

39033926
$result = $ability->execute($input);
@@ -4283,6 +4306,10 @@ function ( $wt ) {
42834306
$this->render_worktree_bounded_cleanup_eligible_apply_result($result, $assoc_args);
42844307
return;
42854308

4309+
case 'cleanup-eligible-drain':
4310+
$this->render_worktree_cleanup_eligible_drain_result($result, $assoc_args);
4311+
return;
4312+
42864313
case 'emergency-cleanup':
42874314
$this->render_worktree_emergency_cleanup_result($result, $assoc_args);
42884315
return;
@@ -6048,6 +6075,95 @@ private function render_worktree_bounded_cleanup_eligible_apply_result( array $r
60486075
}
60496076
}
60506077

6078+
/**
6079+
* Render cleanup-eligible drain output.
6080+
*
6081+
* @param array $result Drain result.
6082+
* @param array $assoc_args CLI args.
6083+
* @return void
6084+
*/
6085+
private function render_worktree_cleanup_eligible_drain_result( array $result, array $assoc_args ): void {
6086+
if ( 'json' === (string) ( $assoc_args['format'] ?? '' ) ) {
6087+
$this->renderer()->json($result);
6088+
return;
6089+
}
6090+
6091+
$summary = (array) ( $result['summary'] ?? array() );
6092+
$final_free_space = (array) ( $summary['final_free_space'] ?? array() );
6093+
WP_CLI::log('Cleanup-eligible drain summary:');
6094+
$this->format_items(
6095+
array(
6096+
array(
6097+
'metric' => 'mode',
6098+
'value' => ! empty($result['applied']) ? 'apply' : 'preview',
6099+
),
6100+
array(
6101+
'metric' => 'passes',
6102+
'value' => (int) ( $summary['passes'] ?? 0 ),
6103+
),
6104+
array(
6105+
'metric' => 'processed',
6106+
'value' => (int) ( $summary['processed'] ?? 0 ),
6107+
),
6108+
array(
6109+
'metric' => 'would_remove',
6110+
'value' => (int) ( $summary['would_remove'] ?? 0 ),
6111+
),
6112+
array(
6113+
'metric' => 'removed',
6114+
'value' => (int) ( $summary['removed'] ?? 0 ),
6115+
),
6116+
array(
6117+
'metric' => 'skipped',
6118+
'value' => (int) ( $summary['skipped'] ?? 0 ),
6119+
),
6120+
array(
6121+
'metric' => 'bytes_reclaimed',
6122+
'value' => $this->format_bytes( (int) ( $summary['bytes_reclaimed'] ?? 0 ) ),
6123+
),
6124+
array(
6125+
'metric' => 'stop_reason',
6126+
'value' => (string) ( $summary['stop_reason'] ?? '' ),
6127+
),
6128+
array(
6129+
'metric' => 'final_free_space',
6130+
'value' => (string) ( $final_free_space['free_human'] ?? 'unknown' ),
6131+
),
6132+
),
6133+
array( 'metric', 'value' ),
6134+
array( 'format' => 'table' ),
6135+
'metric'
6136+
);
6137+
6138+
$passes = (array) ( $result['pass_results'] ?? array() );
6139+
if ( ! empty($passes) ) {
6140+
WP_CLI::log('');
6141+
WP_CLI::log('Pass evidence:');
6142+
$this->format_items(
6143+
array_map(
6144+
fn( $row ) => array(
6145+
'pass' => (int) ( $row['pass'] ?? 0 ),
6146+
'processed' => (int) ( $row['processed'] ?? 0 ),
6147+
'would_remove' => (int) ( $row['would_remove'] ?? 0 ),
6148+
'removed' => (int) ( $row['removed'] ?? 0 ),
6149+
'skipped' => (int) ( $row['skipped'] ?? 0 ),
6150+
'remaining_total' => (int) ( $row['remaining_total'] ?? 0 ),
6151+
'bytes' => $this->format_bytes($row['bytes_reclaimed'] ?? 0),
6152+
),
6153+
$passes
6154+
),
6155+
array( 'pass', 'processed', 'would_remove', 'removed', 'skipped', 'remaining_total', 'bytes' ),
6156+
array( 'format' => 'table' ),
6157+
'pass'
6158+
);
6159+
}
6160+
6161+
if ( ! empty($result['next_commands']) ) {
6162+
WP_CLI::log('');
6163+
WP_CLI::log('Next command: ' . (string) ( (array) $result['next_commands'] )[0]);
6164+
}
6165+
}
6166+
60516167
/**
60526168
* Compact bounded cleanup JSON for chat/operator output.
60536169
*

0 commit comments

Comments
 (0)