Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5886,7 +5886,7 @@ private function render_worktree_artifact_cleanup_result( array $result, array $

if ( $dry_run ) {
$apply_command = (string) ( $result['apply_command'] ?? $summary['apply_command'] ?? 'studio wp datamachine-code workspace cleanup run --mode=artifacts --format=json' );
WP_CLI::success(sprintf('%d artifact(s) would be removed. Apply this page with `%s`; --apply-plan remains a low-level escape hatch.', (int) ( $summary['would_remove_artifacts'] ?? 0 ), $apply_command));
WP_CLI::success(sprintf('%d artifact(s) would be removed. Apply reviewed artifact cleanup with `%s`; --apply-plan remains a low-level escape hatch.', (int) ( $summary['would_remove_artifacts'] ?? 0 ), $apply_command));
return;
}
WP_CLI::success(sprintf('Removed %d artifact(s); %d worktree(s) skipped.', (int) ( $summary['removed_artifacts'] ?? 0 ), count($skipped)));
Expand Down
57 changes: 47 additions & 10 deletions inc/Workspace/WorkspaceArtifactCleanup.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_E
if ( $exhaustive || $full_workspace ) {
$limit = 0;
}
$apply_command = $this->build_artifact_cleanup_apply_command();
$apply_command = $this->build_artifact_cleanup_apply_command();
$preview_command = $this->build_artifact_cleanup_preview_command($opts);
// Apply paths default to safety probing (small subset). Dry-run defaults
// to skipping the per-worktree git probes unless explicitly requested or
// the caller asked for exhaustive mode.
Expand All @@ -67,7 +68,7 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_E
}

if ( ! $dry_run && null === $apply_plan ) {
return new \WP_Error('artifact_cleanup_plan_required', sprintf('Artifact cleanup applies through the high-level cleanup runner for daily cleanup. Run `%s` to apply the same bounded page, or use --dry-run first and --apply-plan=<file> only as a low-level escape hatch.', $apply_command), array( 'status' => 400 ));
return new \WP_Error('artifact_cleanup_plan_required', sprintf('Artifact cleanup applies through the high-level cleanup runner for daily cleanup. Run `%s` to apply reviewed artifact cleanup, or use --dry-run first and --apply-plan=<file> only as a low-level escape hatch.', $apply_command), array( 'status' => 400 ));
}

$only_handles = null;
Expand Down Expand Up @@ -141,13 +142,19 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_E

if ( $dry_run ) {
$response = array(
'success' => true,
'dry_run' => true,
'apply_command' => $apply_command,
'candidates' => $candidates,
'removed' => array(),
'skipped' => $skipped,
'summary' => array( 'apply_command' => $apply_command ) + $summary,
'success' => true,
'dry_run' => true,
'apply_command' => $apply_command,
'preview_command' => $preview_command,
'rerun_preview_command' => $preview_command,
'candidates' => $candidates,
'removed' => array(),
'skipped' => $skipped,
'summary' => array(
'apply_command' => $apply_command,
'preview_command' => $preview_command,
'rerun_preview_command' => $preview_command,
) + $summary,
);
if ( null !== $pagination ) {
$response['pagination'] = $pagination;
Expand Down Expand Up @@ -209,7 +216,37 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_E
* @return string
*/
private function build_artifact_cleanup_apply_command(): string {
return 'studio wp datamachine-code workspace cleanup run --mode=artifacts --dry-run --format=json';
return 'studio wp datamachine-code workspace cleanup run --mode=artifacts';
}

/**
* Build the preview command for the current artifact cleanup dry-run.
*
* @param array<string,mixed> $opts Dry-run options.
* @return string
*/
private function build_artifact_cleanup_preview_command( array $opts ): string {
$parts = array( 'studio wp datamachine-code workspace worktree cleanup-artifacts --dry-run' );
if ( ! empty($opts['force']) ) {
$parts[] = '--force';
}
if ( isset($opts['limit']) ) {
$parts[] = '--limit=' . (int) $opts['limit'];
}
if ( isset($opts['offset']) && (int) $opts['offset'] > 0 ) {
$parts[] = '--offset=' . (int) $opts['offset'];
}
if ( ! empty($opts['exhaustive']) ) {
$parts[] = '--exhaustive';
}
if ( ! empty($opts['safety_probes']) ) {
$parts[] = '--safety-probes';
}
if ( isset($opts['sort']) && '' !== trim( (string) $opts['sort']) ) {
$parts[] = '--sort=' . preg_replace('/[^a-z0-9_\-]/i', '', (string) $opts['sort']);
}
$parts[] = '--format=json';
return implode(' ', $parts);
}

/**
Expand Down
28 changes: 28 additions & 0 deletions inc/Workspace/WorkspaceCleanupPlan.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public function workspace_cleanup_plan( array $opts = array() ): array|\WP_Error
if ( $artifact_plan instanceof \WP_Error ) {
return $artifact_plan;
}
$artifact_plan = $this->normalize_cleanup_plan_child_preview_commands($artifact_plan);
}

$worktree_plan = array(
Expand Down Expand Up @@ -376,6 +377,7 @@ private function build_cleanup_plan_summary( array $rows, array $blocked = array
$category_total = array_sum(array_map('intval', $category_totals));

return array(
'apply_command' => 'studio wp datamachine-code workspace cleanup apply <run-id>',
'total_rows' => $total_rows,
'rows_by_type' => $counts,
'byte_totals' => $byte_totals,
Expand All @@ -390,6 +392,32 @@ private function build_cleanup_plan_summary( array $rows, array $blocked = array
);
}

/**
* Keep embedded child dry-runs from advertising preview commands as applies.
*
* @param array<string,mixed> $plan Child artifact cleanup plan.
* @return array<string,mixed>
*/
private function normalize_cleanup_plan_child_preview_commands( array $plan ): array {
$preview_command = (string) ( $plan['preview_command'] ?? $plan['rerun_preview_command'] ?? $plan['apply_command'] ?? '' );
unset($plan['apply_command']);
if ( '' !== $preview_command ) {
$plan['preview_command'] = $preview_command;
$plan['rerun_preview_command'] = $preview_command;
}

if ( isset($plan['summary']) && is_array($plan['summary']) ) {
$summary_preview_command = (string) ( $plan['summary']['preview_command'] ?? $plan['summary']['rerun_preview_command'] ?? $plan['summary']['apply_command'] ?? $preview_command );
unset($plan['summary']['apply_command']);
if ( '' !== $summary_preview_command ) {
$plan['summary']['preview_command'] = $summary_preview_command;
$plan['summary']['rerun_preview_command'] = $summary_preview_command;
}
}

return $plan;
}

/**
* Build operator continuation evidence from bounded child cleanup plans.
*
Expand Down
105 changes: 105 additions & 0 deletions tests/artifact-cleanup-plan-output-contract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

if ( ! defined('ABSPATH') ) {
define('ABSPATH', __DIR__ . '/fixtures/');
}

require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/inc/Workspace/WorkspaceCleanupPlan.php';

use DataMachineCode\Workspace\WorkspaceCleanupPlan;

function artifact_cleanup_plan_contract_assert( bool $condition, string $message ): void {
if ( ! $condition ) {
throw new RuntimeException($message);
}
}

final class ArtifactCleanupPlanContractWorkspace {
use WorkspaceCleanupPlan;

public const CLEANUP_PLAN_DEFAULT_LIMIT = 100;
public const CLEANUP_PLAN_DEFAULT_BUDGET = '30s';

private string $workspace_path = '/tmp/dmc-artifact-cleanup-contract';

public function worktree_cleanup_artifacts( array $opts = array() ): array {
$preview_command = 'studio wp datamachine-code workspace cleanup run --mode=artifacts --dry-run --format=json';

return array(
'success' => true,
'dry_run' => true,
'apply_command' => $preview_command,
'candidates' => array(
array(
'handle' => 'repo@example',
'repo' => 'repo',
'branch' => 'example',
'path' => '/tmp/dmc-artifact-cleanup-contract/repo@example',
'artifact_size_bytes' => 123,
'artifacts' => array(
array(
'path' => 'vendor',
'size_bytes' => 123,
),
),
),
),
'skipped' => array(),
'summary' => array(
'apply_command' => $preview_command,
'would_remove_artifacts' => 1,
'artifact_size_bytes' => 123,
),
);
}

public function worktree_cleanup_merged( array $opts = array() ): array {
return array(
'success' => true,
'dry_run' => true,
'candidates' => array(),
'skipped' => array(),
'summary' => array(),
);
}

private function stable_cleanup_hash( array $data, string $prefix ): string {
return $prefix . '-' . substr(hash('sha256', wp_json_encode($data)), 0, 12);
}
}

if ( ! function_exists('wp_json_encode') ) {
function wp_json_encode( $data, $options = 0, $depth = 512 ) {
return json_encode($data, $options, $depth);
}
}

$workspace = new ArtifactCleanupPlanContractWorkspace();
$plan = $workspace->workspace_cleanup_plan(array( 'include_worktrees' => false ));

artifact_cleanup_plan_contract_assert(is_array($plan), 'cleanup plan should return an array');
artifact_cleanup_plan_contract_assert(
'studio wp datamachine-code workspace cleanup apply <run-id>' === ( $plan['summary']['apply_command'] ?? '' ),
'cleanup plan summary should expose the authoritative apply command'
);

$artifact_plan = $plan['plans']['artifact_cleanup'] ?? array();
artifact_cleanup_plan_contract_assert(! array_key_exists('apply_command', $artifact_plan), 'nested artifact plan should not expose apply_command');
artifact_cleanup_plan_contract_assert(! array_key_exists('apply_command', $artifact_plan['summary'] ?? array()), 'nested artifact summary should not expose apply_command');
artifact_cleanup_plan_contract_assert(
'studio wp datamachine-code workspace cleanup run --mode=artifacts --dry-run --format=json' === ( $artifact_plan['preview_command'] ?? '' ),
'nested artifact plan should expose preview_command'
);
artifact_cleanup_plan_contract_assert(
( $artifact_plan['preview_command'] ?? null ) === ( $artifact_plan['rerun_preview_command'] ?? null ),
'nested artifact plan should expose matching rerun_preview_command'
);
artifact_cleanup_plan_contract_assert(
( $artifact_plan['summary']['preview_command'] ?? null ) === ( $artifact_plan['summary']['rerun_preview_command'] ?? null ),
'nested artifact summary should expose matching preview commands'
);

fwrite(STDOUT, "artifact-cleanup-plan-output-contract ok\n");
Loading