Skip to content

Get Blueprints v2 to work with Playground CLI #128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 18 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function __construct( Client $client, ?string $tmpRoot = null ) {
$this->tmpRoot = $tmpRoot ?: wp_unix_sys_get_temp_dir();
}

public function setExecutionContext( Filesystem $executionContext ) {
public function setExecutionContext( ?Filesystem $executionContext ) {
$this->executionContext = $executionContext;
}

Expand Down
73 changes: 51 additions & 22 deletions components/Blueprints/Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@
use function WordPress\Zip\is_zip_file_stream;

class Runner {
const EXECUTION_MODE_CREATE_NEW_SITE = 'create-new-site';
const EXECUTION_MODE_APPLY_TO_EXISTING_SITE = 'apply-to-existing-site';
const EXECUTION_MODES = [
self::EXECUTION_MODE_CREATE_NEW_SITE,
self::EXECUTION_MODE_APPLY_TO_EXISTING_SITE,
];

/**
* @var RunnerConfiguration
*/
Expand Down Expand Up @@ -113,7 +120,7 @@ public function __construct( RunnerConfiguration $configuration ) {
$this->configuration = $configuration;
$this->validateConfiguration( $configuration );

$this->client = new Client();
$this->client = apply_filters('blueprint.http_client', new Client());
$this->mainTracker = new Tracker();

// Set up progress logging
Expand All @@ -134,17 +141,17 @@ private function validateConfiguration( RunnerConfiguration $config ): void {

// Validate execution mode
$mode = $config->getExecutionMode();
if ( ! in_array( $mode, [ 'create-new-site', 'apply-to-existing-site' ], true ) ) {
throw new BlueprintExecutionException( "Execution mode must be either 'create-new-site' or 'apply-to-existing-site'." );
if ( ! in_array( $mode, self::EXECUTION_MODES, true ) ) {
throw new BlueprintExecutionException( "Execution mode must be one of: " . implode( ', ', self::EXECUTION_MODES ) );
}

// Validate site URL
// Note: $options is not defined in this context, so we skip this block.
// If you want to validate the site URL, you should use $config->getTargetSiteUrl().
$siteUrl = $config->getTargetSiteUrl();
if ( $mode === 'create-new-site' ) {
if ( $mode === self::EXECUTION_MODE_CREATE_NEW_SITE ) {
if ( empty( $siteUrl ) ) {
throw new BlueprintExecutionException( "Site URL is required when the execution mode is 'create-new-site'." );
throw new BlueprintExecutionException( sprintf( "Site URL is required when the execution mode is '%s'.", self::EXECUTION_MODE_CREATE_NEW_SITE ) );
}
}
if ( ! empty( $siteUrl ) && ! filter_var( $siteUrl, FILTER_VALIDATE_URL ) ) {
Expand Down Expand Up @@ -194,6 +201,7 @@ private function validateConfiguration( RunnerConfiguration $config ): void {

public function run(): void {
$tempRoot = wp_unix_sys_get_temp_dir() . '/wp-blueprints-runtime-' . uniqid();

// TODO: Are there cases where we should not have these permissions?
mkdir( $tempRoot, 0777, true );

Expand Down Expand Up @@ -226,8 +234,13 @@ public function run(): void {
$targetSiteFs = LocalFilesystem::create( $this->configuration->getTargetSiteRoot() );
$wpCliReference = DataReference::create( 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar' );

$execution_context_root = $this->blueprintExecutionContext->get_meta()['root'];
assert(is_string($execution_context_root) && strlen($execution_context_root) > 0, 'Assertion failed: Execution context root was empty.');
$execution_context = $this->blueprintExecutionContext->get_meta();
if(
isset($execution_context['root']) &&
( !is_string($execution_context['root']) || strlen($execution_context['root']) === 0)
) {
throw new BlueprintExecutionException('Execution context was a local directory, but the Runner could not determine the root directory. This should never happen. Please report this as a bug.');
}

$this->runtime = new Runtime(
$targetSiteFs,
Expand All @@ -237,7 +250,7 @@ public function run(): void {
$this->blueprintArray,
$tempRoot,
$wpCliReference,
$execution_context_root
isset($execution_context['root']) ? $execution_context['root'] : null
);
$this->progressObserver->setRuntime( $this->runtime );
$progress['wpCli']->setCaption( 'Downloading WP-CLI' );
Expand All @@ -246,16 +259,21 @@ public function run(): void {
], $progress['wpCli'] );

$progress['targetResolution']->setCaption( 'Resolving target site' );
if ( $this->configuration->getExecutionMode() === 'apply-to-existing-site' ) {
if ( $this->configuration->getExecutionMode() === self::EXECUTION_MODE_APPLY_TO_EXISTING_SITE ) {
ExistingSiteResolver::resolve( $this->runtime, $progress['targetResolution'], $this->wpVersionConstraint );
} else {
NewSiteResolver::resolve( $this->runtime, $progress['targetResolution'], $this->wpVersionConstraint, $this->recommendedWpVersion );
}
$progress['targetResolution']->finish();

do_action('blueprint.target_resolved');

$progress['data']->setCaption( 'Resolving data references' );
$this->assets->startEagerResolution( $this->dataReferencesToAutoResolve, $progress['data'] );
$this->executePlan( $progress['execution'], $plan, $this->runtime );

// @TODO: Assert WordPress is still correctly installed

$progress->finish();
} finally {
// TODO: Optionally preserve workspace in case of error? Support resuming after error?
Expand Down Expand Up @@ -288,7 +306,14 @@ private function loadBlueprint() {
$blueprintString = $resolved->getStream()->consume_all();
$this->blueprintExecutionContext = LocalFilesystem::create( dirname( $reference->get_path() ) );
} else {
// For the purposes of Blueprint resolution, the execution context is the
// current working directory. This way, a path such as ./blueprint.json
// will mean "a blueprint.json file in the current working directory" and not
// "a ./blueprint.json path without a point of reference".
$this->assets->setExecutionContext( LocalFilesystem::create( getcwd() ) );
$resolved = $this->assets->resolve( $reference );
$this->assets->setExecutionContext( null );

if ( $resolved instanceof File ) {
$stream = $resolved->getStream();

Expand Down Expand Up @@ -376,6 +401,8 @@ private function validateBlueprint(): void {

$this->configuration->getLogger()->debug( 'Final resolved Blueprint: ' . json_encode( $this->blueprintArray, JSON_PRETTY_PRINT ) );

$this->blueprintArray = apply_filters( 'blueprint.resolved', $this->blueprintArray );

// Assert the Blueprint conforms to the latest JSON schema.
$v = new HumanFriendlySchemaValidator(
json_decode( file_get_contents( __DIR__ . '/Versions/Version2/json-schema/schema-v2.json' ), true )
Expand Down Expand Up @@ -438,7 +465,7 @@ private function validateBlueprint(): void {
// WordPress Version Constraint
if ( isset( $this->blueprintArray['wordpressVersion'] ) ) {
$wp_version = $this->blueprintArray['wordpressVersion'];
$recommended = null;
$min = $max = $recommended = null;
if ( is_string( $wp_version ) ) {
$this->recommendedWpVersion = $wp_version;
$recommended = WordPressVersion::fromString( $wp_version );
Expand All @@ -465,6 +492,8 @@ private function validateBlueprint(): void {
$this->recommendedWpVersion = $wp_version['max'];
$max = WordPressVersion::fromString( $wp_version['max'] );
if ( ! $max ) {
// @TODO: Reuse this error message
// 'Unrecognized WordPress version. Please use "latest", a URL, or a numeric version such as "6.2", "6.0.1", "6.2-beta1", or "6.2-RC1"'
throw new BlueprintExecutionException( 'Invalid WordPress version string in wordpressVersion.max: ' . $wp_version['max'] );
}
}
Expand All @@ -486,6 +515,14 @@ private function validateBlueprint(): void {
// correctly. The actual version check for WordPress is done in
// NewSiteResolver and ExistingSiteResolver.
}

// Validate the override constraint if it was set
if ( $this->wpVersionConstraint ) {
$wpConstraintErrors = $this->wpVersionConstraint->validate();
if ( ! empty( $wpConstraintErrors ) ) {
throw new BlueprintExecutionException( 'Invalid WordPress version constraint from CLI override: ' . implode( '; ', $wpConstraintErrors ) );
}
}
}

private function createExecutionPlan(): array {
Expand Down Expand Up @@ -626,9 +663,9 @@ private function createExecutionPlan(): array {
// @TODO: Make sure this doesn't get included twice in the execution plan,
// e.g. if the Blueprint specified this step manually.
if ( $step instanceof ImportContentStep ) {
if($this->configuration->isRunningAsPhar()) {
throw new InvalidArgumentException( '@TODO: Importing content is not supported when running as phar.' );
} else {
// if($this->configuration->isRunningAsPhar()) {
// throw new InvalidArgumentException( '@TODO: Importing content is not supported when running as phar.' );
// } else {
$libraries_phar_path = __DIR__ . '/../../dist/php-toolkit.phar';
if(!file_exists($libraries_phar_path)) {
throw new InvalidArgumentException(
Expand All @@ -641,7 +678,7 @@ private function createExecutionPlan(): array {
'filename' => 'php-toolkit.phar',
'content' => file_get_contents( $libraries_phar_path )
] ) );
}
// }
array_unshift( $plan, $this->createStepObject( 'writeFiles', [
'files' => [
'php-toolkit.phar' => $source,
Expand Down Expand Up @@ -920,14 +957,6 @@ static function () {

return new WriteFilesStep( $files );

case 'runPHP':
return new RunPHPStep(
$this->createDataReference( [
'filename' => 'run-php.php',
'content' => $data['code'],
] ),
$data['env'] ?? []
);
case 'unzip':
$zipFile = $this->createDataReference( $data['zipFile'], [ ExecutionContextPath::class ] );

Expand Down
2 changes: 1 addition & 1 deletion components/Blueprints/RunnerConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class RunnerConfiguration {
/**
* @var string
*/
private $mode = 'create-new-site'; // or apply-to-existing-site
private $mode = Runner::EXECUTION_MODE_CREATE_NEW_SITE;
/**
* @var string
*/
Expand Down
9 changes: 5 additions & 4 deletions components/Blueprints/Runtime.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public function __construct(
array $blueprint,
string $tempRoot,
DataReference $wpCliReference,
string $executionContextRoot
?string $executionContextRoot=null
) {
$this->targetFs = $targetFs;
$this->configuration = $configuration;
Expand All @@ -87,7 +87,7 @@ public function __construct(
$this->executionContextRoot = $executionContextRoot;
}

public function getExecutionContextRoot(): string {
public function getExecutionContextRoot(): ?string {
return $this->executionContextRoot;
}

Expand Down Expand Up @@ -231,8 +231,9 @@ public function evalPhpCodeInSubProcess(
$process = $this->createPhpSubProcess( $code, $env, $input, $timeout );
$process->mustRun();

$output = $process->getOutputStream(Process::OUTPUT_FILE)->consume_all();
return new EvalResult(
$process->getOutputStream(Process::OUTPUT_FILE)->consume_all(),
$output,
$process
);
}
Expand Down Expand Up @@ -262,7 +263,7 @@ public function createPhpSubProcess(
$phpBinary = null;
if ( getenv('PHP_BINARY') ) {
$phpBinary = getenv('PHP_BINARY');
} elseif ( PHP_SAPI === 'cli' && isset($_SERVER['argv'][0]) ) {
} elseif ( PHP_BINARY ) {
$phpBinary = PHP_BINARY;
} else {
$phpBinary = 'php';
Expand Down
68 changes: 50 additions & 18 deletions components/Blueprints/SiteResolver/NewSiteResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use WordPress\Blueprints\Runtime;
use WordPress\Blueprints\VersionStrings\VersionConstraint;
use WordPress\HttpClient\Client;
use WordPress\HttpClient\Request;
use WordPress\Zip\ZipFilesystem;

use function WordPress\Filesystem\copy_between_filesystems;
Expand Down Expand Up @@ -66,6 +67,10 @@ static public function resolve( Runtime $runtime, Tracker $progress, ?VersionCon

// If SQLite integration zip provided, unzip into appropriate folder
if ( $runtime->getConfiguration()->getDatabaseEngine() === 'sqlite' ) {
/*
* @TODO: Ensure DB_NAME gets defined in wp-config.php before installing the SQLite plugin.
*/

$progress['resolve_assets']->setCaption( 'Downloading SQLite integration plugin' );
$resolved = $runtime->resolve( $assets['sqlite-integration'] );
if ( ! $resolved instanceof File ) {
Expand Down Expand Up @@ -96,22 +101,7 @@ static public function resolve( Runtime $runtime, Tracker $progress, ?VersionCon
// Technically, this is a "new site" resolver, but it's entirely possible
// the developer-provided WordPress zip already has a sqlite database with the
// a WordPress site installed..
$installCheck = $runtime->evalPhpCodeInSubProcess(
<<<'PHP'
<?php
$wp_load = getenv('DOCROOT') . '/wp-load.php';
if (!file_exists($wp_load)) {
append_output('0');
exit;
}
require $wp_load;

append_output( function_exists('is_blog_installed') && is_blog_installed() ? '1' : '0' );
PHP

)->outputFileContent;

if ( trim( $installCheck ) !== '1' ) {
if ( ! self::isWordPressInstalled( $runtime, $progress ) ) {
if ( ! $targetFs->exists( '/wp-config.php' ) ) {
if ( $targetFs->exists( 'wp-config-sample.php' ) ) {
$targetFs->copy( 'wp-config-sample.php', 'wp-config.php' );
Expand All @@ -129,6 +119,8 @@ static public function resolve( Runtime $runtime, Tracker $progress, ?VersionCon
$wp_cli_path,
'core',
'install',
'--path=' . $runtime->getConfiguration()->getTargetSiteRoot(),

// For Docker compatibility. If we got this far, Blueprint runner was already
// allowed to run as root.
'--allow-root',
Expand All @@ -140,10 +132,39 @@ static public function resolve( Runtime $runtime, Tracker $progress, ?VersionCon
'--skip-email',
] );
$process->mustRun();

if ( ! self::isWordPressInstalled( $runtime, $progress ) ) {
// @TODO: This breaks in Playground CLI
throw new BlueprintExecutionException( 'WordPress installation failed' );
}
}
$progress->finish();
}

static private function isWordPressInstalled( Runtime $runtime, Tracker $progress ) {
$installCheck = $runtime->evalPhpCodeInSubProcess(
<<<'PHP'
<?php
$wp_load = getenv('DOCROOT') . '/wp-load.php';
if (!file_exists($wp_load)) {
append_output('0');
exit;
}
require $wp_load;

append_output( function_exists('is_blog_installed') && is_blog_installed() ? '1' : '0' );
PHP
,
[
'DOCROOT' => $runtime->getConfiguration()->getTargetSiteRoot(),
],
null,
5
)->outputFileContent;

return trim( $installCheck ) === '1';
}

static private function resolveWordPressZipUrl( Client $client, string $version_string ): string {
if ( $version_string === 'latest' ) {
return 'https://wordpress.org/latest.zip';
Expand All @@ -160,13 +181,19 @@ static private function resolveWordPressZipUrl( Client $client, string $version_
return 'https://wordpress.org/nightly-builds/wordpress-latest.zip';
}

$latestVersions = $client->fetch( 'https://api.wordpress.org/core/version-check/1.7/?channel=beta' )->json();
$latestVersions = $client->fetch( new Request( 'https://api.wordpress.org/core/version-check/1.7/?channel=beta' ) )->json();

$latestVersions = array_filter( $latestVersions['offers'], function ( $v ) {
return $v['response'] === 'autoupdate';
} );

$latestNonBeta = null;

foreach ( $latestVersions as $apiVersion ) {
// Keep track of the first non-beta version (which is the latest)
if ( $latestNonBeta === null && strpos( $apiVersion['version'], 'beta' ) === false ) {
$latestNonBeta = $apiVersion;
}

if ( $version_string === 'beta' && strpos( $apiVersion['version'], 'beta' ) !== false ) {
return $apiVersion['download'];
} elseif (
Expand All @@ -189,6 +216,11 @@ static private function resolveWordPressZipUrl( Client $client, string $version_
return $apiVersion['download'];
}
}

// If we're looking for beta but no beta was found, return latest
if ( $version_string === 'beta' && $latestNonBeta !== null ) {
return $latestNonBeta['download'];
}

/**
* If we didn't get a useful match in the API response, it could be version that's not
Expand Down
7 changes: 5 additions & 2 deletions components/Blueprints/Steps/ImportContentStep.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ private function importWxr( Runtime $runtime, array $content_definition, Tracker
<?php
run_content_import([
'mode' => 'wxr',
'execution_context_root' => getenv('EXECUTION_CONTEXT'),
'execution_context_root' => getenv('EXECUTION_CONTEXT') ? getenv('EXECUTION_CONTEXT') : null,
'source' => json_decode(getenv('DATA_SOURCE_DEFINITION'), true),
// @TODO: Support arbitrary media URLs to enable fetching assets during import.
// 'media_url' => 'https://pd.w.org/'
Expand Down Expand Up @@ -99,8 +99,11 @@ private function importWxr( Runtime $runtime, array $content_definition, Tracker
break;
case 'error':
throw new BlueprintExecutionException( $data['message'] );
case 'completion':
$progress->finish();
break;
default:
throw new BlueprintExecutionException( 'Unknown messagetype: ' . $data['type'] );
throw new BlueprintExecutionException( 'Unknown message type: ' . $data['type'] );
}
}

Expand Down
Loading
Loading