Skip to content

Commit 6171c47

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/issue-675-stale-worktree-cleanup
# Conflicts: # inc/Abilities/WorkspaceAbilities.php # inc/Cli/Commands/WorkspaceCommand.php
2 parents 10cb361 + 841d3ac commit 6171c47

7 files changed

Lines changed: 485 additions & 189 deletions

inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,17 @@ public function read( string $run_id, bool $include_evidence = false, bool $incl
3737
$child_jobs = $this->get_cleanup_run_descendant_jobs($job_id);
3838
$aggregate = $this->aggregate_cleanup_child_jobs($child_jobs);
3939
$children = $aggregate['children'];
40-
$state = $this->cleanup_run_state( (string) ( $job['status'] ?? '' ), $children);
40+
$parent_status = (string) ( $job['status'] ?? '' );
41+
$state = $this->cleanup_run_state($parent_status, $children, $parent_result, $aggregate);
4142

4243
$children_for_output = ( $include_evidence || $include_details ) ? $children : $this->summarize_cleanup_children($children);
4344
$output = array(
4445
'success' => true,
4546
'state' => $state,
4647
'run_id' => $this->cleanup_run_id($job_id),
4748
'job_id' => $job_id,
48-
'status' => in_array($state, array( 'children_processing', 'partial_failed' ), true) ? $state : ( $job['status'] ?? '' ),
49+
'status' => $state,
50+
'parent_status' => $parent_status,
4951
'created_at' => $job['created_at'] ?? '',
5052
'parent_completed_at' => $job['completed_at'] ?? '',
5153
'artifact_cleanup' => $aggregate['artifact_cleanup'],
@@ -132,6 +134,7 @@ private function aggregate_cleanup_child_jobs( array $child_jobs ): array {
132134
'processing' => 0,
133135
'completed' => 0,
134136
'failed' => 0,
137+
'skipped' => 0,
135138
'running' => 0,
136139
'total' => 0,
137140
'statuses' => array(),
@@ -218,6 +221,7 @@ private function summarize_cleanup_children( array $children ): array {
218221
'processing' => (int) ( $children['processing'] ?? 0 ),
219222
'completed' => (int) ( $children['completed'] ?? 0 ),
220223
'failed' => (int) ( $children['failed'] ?? 0 ),
224+
'skipped' => (int) ( $children['skipped'] ?? 0 ),
221225
'running' => (int) ( $children['running'] ?? 0 ),
222226
'total' => (int) ( $children['total'] ?? 0 ),
223227
'statuses' => (array) ( $children['statuses'] ?? array() ),
@@ -255,9 +259,9 @@ private function cleanup_run_drain_summary( int $job_id, string $state, array $c
255259
)
256260
)
257261
);
258-
$run_id = $this->cleanup_run_id($job_id);
259-
$cleanup_items = (array) ( $aggregate['cleanup_items'] ?? array() );
260-
$commands = array(
262+
$run_id = $this->cleanup_run_id($job_id);
263+
$cleanup_items = (array) ( $aggregate['cleanup_items'] ?? array() );
264+
$commands = array(
261265
'parent' => sprintf('studio wp datamachine drain --job-id=%d', $job_id),
262266
'verify' => sprintf('studio wp datamachine-code workspace cleanup status %s --format=json', $run_id),
263267
);
@@ -266,7 +270,7 @@ private function cleanup_run_drain_summary( int $job_id, string $state, array $c
266270
}
267271

268272
return array(
269-
'needed' => in_array($state, array( 'running', 'children_processing' ), true),
273+
'needed' => in_array($state, array( 'running', 'waiting_on_children' ), true),
270274
'commands' => $commands,
271275
'active_child_job_ids' => $active_child_ids,
272276
'bytes_reclaimed' => (int) ( $cleanup_items['bytes_reclaimed'] ?? 0 ),
@@ -478,6 +482,10 @@ private function count_cleanup_child_status( array &$children, string $status ):
478482
++$children['failed'];
479483
return;
480484
}
485+
if ( str_starts_with($status, 'skipped') ) {
486+
++$children['skipped'];
487+
return;
488+
}
481489
if ( str_starts_with($status, 'completed') ) {
482490
++$children['completed'];
483491
}
@@ -546,9 +554,6 @@ private function merge_cleanup_reason_examples( array &$examples, array $rows ):
546554
private function sum_cleanup_rows_bytes( array $rows, array $fields ): int {
547555
$total = 0;
548556
foreach ( $rows as $row ) {
549-
if ( ! is_array($row) ) {
550-
continue;
551-
}
552557
$found = false;
553558
foreach ( $fields as $field ) {
554559
if ( isset($row[ $field ]) ) {
@@ -574,22 +579,69 @@ private function sum_cleanup_rows_bytes( array $rows, array $fields ): int {
574579
* @param array $children Child summary.
575580
* @return string
576581
*/
577-
private function cleanup_run_state( string $status, array $children ): string {
582+
private function cleanup_run_state( string $status, array $children, array $parent_result, array $aggregate ): string {
578583
$parent_state = $this->cleanup_job_state($status);
579-
if ( in_array($parent_state, array( 'cancelled', 'partial_failure' ), true) ) {
584+
if ( in_array($parent_state, array( 'cancelled', 'failed' ), true) ) {
580585
return $parent_state;
581586
}
582587

583588
if ( (int) ( $children['running'] ?? 0 ) > 0 ) {
584-
return 'children_processing';
589+
return 'waiting_on_children';
585590
}
586591
if ( (int) ( $children['failed'] ?? 0 ) > 0 ) {
587-
return 'partial_failed';
592+
return 'failed';
593+
}
594+
595+
$child_total = (int) ( $children['total'] ?? 0 );
596+
$terminal_child = (int) ( $children['completed'] ?? 0 ) + (int) ( $children['skipped'] ?? 0 );
597+
if ( $child_total > 0 && $terminal_child >= $child_total ) {
598+
return 'complete';
599+
}
600+
601+
if ( 0 === $child_total && $this->cleanup_parent_has_no_planned_work($parent_result, $aggregate) ) {
602+
return 'no_work';
588603
}
589604

590605
return $parent_state;
591606
}
592607

608+
/**
609+
* Determine whether parent cleanup evidence proves there was no work to schedule.
610+
*
611+
* @param array $parent_result Parent system task result.
612+
* @param array $aggregate Child aggregate.
613+
* @return bool
614+
*/
615+
private function cleanup_parent_has_no_planned_work( array $parent_result, array $aggregate ): bool {
616+
if ( array() === $parent_result ) {
617+
return false;
618+
}
619+
620+
$cleanup_items = (array) ( $aggregate['cleanup_items'] ?? array() );
621+
if ( (int) ( $cleanup_items['planned_rows'] ?? 0 ) > 0 ) {
622+
return false;
623+
}
624+
625+
if ( array_key_exists('chunks', $parent_result) && array() !== (array) $parent_result['chunks'] ) {
626+
return false;
627+
}
628+
629+
if ( isset($parent_result['evidence']) && is_array($parent_result['evidence']) && (int) ( $parent_result['evidence']['planned_chunks'] ?? 0 ) > 0 ) {
630+
return false;
631+
}
632+
633+
if ( isset($parent_result['chunk_row_counts']) && is_array($parent_result['chunk_row_counts']) ) {
634+
foreach ( $parent_result['chunk_row_counts'] as $count ) {
635+
if ( (int) $count > 0 ) {
636+
return false;
637+
}
638+
}
639+
return true;
640+
}
641+
642+
return array_key_exists('chunks', $parent_result) || array_key_exists('chunk_row_counts', $parent_result);
643+
}
644+
593645
/**
594646
* Convert job status into cleanup run state.
595647
*
@@ -604,7 +656,7 @@ private function cleanup_job_state( string $status ): string {
604656
return 'cancelled';
605657
}
606658
if ( str_starts_with($status, 'failed') ) {
607-
return 'partial_failure';
659+
return 'failed';
608660
}
609661
if ( str_starts_with($status, 'completed') ) {
610662
return 'complete';

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -660,9 +660,9 @@ public function adopt_repo( array $args, array $assoc_args ): void {
660660
* # Review artifact cleanup synchronously (bounded; default limit=100)
661661
* wp datamachine-code workspace cleanup run --mode=artifacts --dry-run
662662
*
663-
* # Walk a huge workspace in 100-worktree pages
664-
* wp datamachine-code workspace cleanup run --mode=artifacts --dry-run --offset=0 --format=json
665-
* wp datamachine-code workspace cleanup run --mode=artifacts --dry-run --offset=100 --format=json
663+
* # Persist a snapshot-safe artifact cleanup plan, then apply it by run ID
664+
* wp datamachine-code workspace cleanup run --mode=artifacts --dry-run --format=json
665+
* wp datamachine-code workspace cleanup apply cleanup-run-20260504120000-abc123
666666
*
667667
* # Full audit (slow on huge workspaces)
668668
* wp datamachine-code workspace cleanup run --mode=artifacts --dry-run --exhaustive --format=json
@@ -939,6 +939,29 @@ private function run_cleanup_plan( array $assoc_args ): void {
939939
return;
940940
}
941941

942+
$input = $this->cleanup_plan_input($mode, $assoc_args);
943+
if ( 'json' !== (string) ( $assoc_args['format'] ?? 'table' ) ) {
944+
$profile = ! empty($input['include_artifacts']) ? 'includes artifact scan' : 'local worktree merge signals';
945+
WP_CLI::log(sprintf('Planning cleanup (%s; %s)...', $mode, $profile));
946+
}
947+
948+
$result = $ability->execute($input);
949+
if ( is_wp_error($result) ) {
950+
WP_CLI::error($result->get_error_message());
951+
return;
952+
}
953+
954+
$this->render_cleanup_plan_result($result, $assoc_args);
955+
}
956+
957+
/**
958+
* Normalize cleanup plan input shared by `cleanup plan` and dry-run `cleanup run`.
959+
*
960+
* @param string $mode Cleanup mode.
961+
* @param array $assoc_args CLI associative args.
962+
* @return array<string,mixed>
963+
*/
964+
private function cleanup_plan_input( string $mode, array $assoc_args ): array {
942965
$include_artifacts = 'artifacts' === $mode || ! empty($assoc_args['include-artifacts']);
943966
$include_worktrees = 'artifacts' !== $mode;
944967
$input = array(
@@ -959,18 +982,8 @@ private function run_cleanup_plan( array $assoc_args ): void {
959982
$input['worktree_older_than'] = '14d';
960983
}
961984
}
962-
if ( 'json' !== (string) ( $assoc_args['format'] ?? 'table' ) ) {
963-
$profile = $include_artifacts ? 'includes artifact scan' : 'local worktree merge signals';
964-
WP_CLI::log(sprintf('Planning cleanup (%s; %s)...', $mode, $profile));
965-
}
966-
967-
$result = $ability->execute($input);
968-
if ( is_wp_error($result) ) {
969-
WP_CLI::error($result->get_error_message());
970-
return;
971-
}
972985

973-
$this->render_cleanup_plan_result($result, $assoc_args);
986+
return $input;
974987
}
975988

976989
private function run_cleanup_control_ability( string $operation, string $run_id, array $assoc_args ): void {
@@ -1022,28 +1035,13 @@ private function run_cleanup_review( array $assoc_args ): void {
10221035
return;
10231036

10241037
case 'artifacts':
1025-
$ability = wp_get_ability('datamachine-code/workspace-worktree-cleanup-artifacts');
1026-
$artifact_input = array(
1027-
'dry_run' => true,
1028-
'force' => ! empty($assoc_args['force']),
1029-
);
1030-
if ( isset($assoc_args['limit']) ) {
1031-
$artifact_input['limit'] = (int) $assoc_args['limit'];
1032-
}
1033-
if ( isset($assoc_args['offset']) ) {
1034-
$artifact_input['offset'] = (int) $assoc_args['offset'];
1035-
}
1036-
if ( ! empty($assoc_args['exhaustive']) ) {
1037-
$artifact_input['exhaustive'] = true;
1038-
}
1039-
if ( ! empty($assoc_args['safety-probes']) ) {
1040-
$artifact_input['safety_probes'] = true;
1041-
}
1042-
if ( isset($assoc_args['sort']) && '' !== trim( (string) $assoc_args['sort']) ) {
1043-
$artifact_input['sort'] = trim( (string) $assoc_args['sort']);
1038+
$ability = wp_get_ability('datamachine-code/workspace-cleanup-plan');
1039+
$result = $ability ? $ability->execute($this->cleanup_plan_input($mode, $assoc_args)) : new \WP_Error('cleanup_plan_ability_missing', 'Workspace cleanup plan ability not registered.');
1040+
if ( is_wp_error($result) ) {
1041+
WP_CLI::error($result->get_error_message());
1042+
return;
10441043
}
1045-
$result = $ability ? $ability->execute($artifact_input) : new \WP_Error('artifact_cleanup_ability_missing', 'Artifact cleanup ability not registered.');
1046-
$this->render_worktree_artifact_cleanup_result_from_ability($result, $assoc_args);
1044+
$this->render_cleanup_plan_result($result, $assoc_args);
10471045
return;
10481046

10491047
case 'emergency':
@@ -1103,14 +1101,6 @@ private function render_worktree_cleanup_result_from_ability( array|\WP_Error $r
11031101
$this->render_worktree_cleanup_result($result, $assoc_args);
11041102
}
11051103

1106-
private function render_worktree_artifact_cleanup_result_from_ability( array|\WP_Error $result, array $assoc_args ): void {
1107-
if ( is_wp_error($result) ) {
1108-
WP_CLI::error($result->get_error_message());
1109-
return;
1110-
}
1111-
$this->render_worktree_artifact_cleanup_result($result, $assoc_args);
1112-
}
1113-
11141104
private function render_worktree_emergency_cleanup_result_from_ability( array|\WP_Error $result, array $assoc_args ): void {
11151105
if ( is_wp_error($result) ) {
11161106
WP_CLI::error($result->get_error_message());

inc/Tasks/WorkspaceRetentionCleanupTask.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,14 +257,11 @@ private function build_cleanup_chunk_rows( Workspace $workspace, array $opts, ar
257257
);
258258

259259
if ( ! empty($opts['artifact_cleanup']) ) {
260-
$artifact_limit = isset($params['limit']) ? max(0, (int) $params['limit']) : 100;
261-
$artifact_page = $workspace->worktree_cleanup_artifacts(
260+
$artifact_page = $workspace->worktree_cleanup_artifacts(
262261
array(
263262
'dry_run' => true,
264263
'force' => ! empty($opts['force']),
265-
'limit' => $artifact_limit,
266-
'offset' => isset($params['offset']) ? max(0, (int) $params['offset']) : 0,
267-
'exhaustive' => ! empty($params['exhaustive']),
264+
'exhaustive' => true,
268265
'safety_probes' => true,
269266
)
270267
);

0 commit comments

Comments
 (0)