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 packages/cli/src/commands/recipe-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ export async function runRecipe(options: RecipeRunOptions, interruption?: Recipe
})
interruption?.throwIfInterrupted()

executions.push(...(await applyRecipeRuntimeSetup({ recipe, recipeDirectory, runtime, prepared: preparedRuntimeSetup, phaseExecutor, interruption })).executions)
executions.push(...(await applyRecipeRuntimeSetup({ recipe, recipeDirectory, runtime, runtimeSpec: runtimeCreateSpec, prepared: preparedRuntimeSetup, phaseExecutor, interruption })).executions)

fixtureDatabases = await phaseTracker.run("import_fixture_databases", phaseFixtureDatabaseData(recipe), async () => await awaitRecipe("fixture-databases.import", importRecipeFixtureDatabases(recipe, recipeDirectory, runtime!, executions)))
const siteSeeds = await awaitRecipe("site-seeds.import", importRecipeSiteSeeds(recipe, recipeDirectory, runtime!, executions))
Expand Down
62 changes: 45 additions & 17 deletions packages/cli/src/commands/recipe-runtime-setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { cp, mkdtemp, rm, stat } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join, posix, resolve } from "node:path"
import { phpRuntimeRecipePluginPreloadFunction, type ExecutionResult, type MountSpec, type Runtime, type WorkspaceRecipe, type WorkspaceRecipeMount, type WorkspaceRecipePluginRuntimeHealthProbe } from "@automattic/wp-codebox-core"
import { phpRuntimeRecipePluginPreloadFunction, type ExecutionResult, type MountSpec, type Runtime, type RuntimeCreateSpec, type WorkspaceRecipe, type WorkspaceRecipeMount, type WorkspaceRecipePluginRuntimeHealthProbe } from "@automattic/wp-codebox-core"
import { requiresManagedMysqlMultisitePreinstall } from "@automattic/wp-codebox-playground"
import { installMuPluginsCode, installPluginComposerAutoloadersCode, prepareRecipeDependencyOverlays, prepareRecipeExtraPlugins, prepareRecipeRuntimeOverlays, prepareRecipeStagedFiles, prepareRecipeWorkspacePreloads, prepareRecipeWorkspaces, recipeMountType, type PreparedDependencyOverlay, type PreparedExtraPlugin, type PreparedRuntimeOverlay, type PreparedStagedFile, type PreparedWorkspaceMount } from "../recipe-sources.js"
import { pluginRuntimeHealthProbeStep, type RecipeWorkflowPhase } from "../recipe-validation.js"
import { pluginRuntimeHealthProbeStepIndex, pluginRuntimeSetupStepIndex } from "../recipe-dry-run.js"
Expand Down Expand Up @@ -52,9 +53,10 @@ export async function applyRecipeRuntimeSetup(args: {
runtime: Runtime
prepared: PreparedRecipeRuntimeSetup
phaseExecutor: RecipeRunPhaseExecutor
runtimeSpec: Pick<RuntimeCreateSpec, "environment" | "runtimeEnv">
interruption?: RecipeInterruptionController
}): Promise<RecipeRuntimeSetupResult> {
const { recipe, recipeDirectory, runtime, prepared, phaseExecutor, interruption } = args
const { recipe, recipeDirectory, runtime, runtimeSpec, prepared, phaseExecutor, interruption } = args
const { workspaceMounts, extraPlugins, dependencyOverlays, stagedFiles, overlays, inputMountBaselinePaths, inputMountPathMap } = prepared
const executions: RecipeExecutionResult[] = []
const phaseTracker = phaseExecutor.tracker
Expand Down Expand Up @@ -127,19 +129,20 @@ export async function applyRecipeRuntimeSetup(args: {
interruption?.throwIfInterrupted()
}

const extraPluginMounts: MountSpec[] = extraPlugins.map((plugin) => ({
type: "directory",
source: plugin.source,
target: plugin.target,
mode: "readonly",
metadata: {
kind: "extra-plugin",
slug: plugin.slug,
source: plugin.provenance,
},
}))
await phaseTracker.run("mount_plugins", phasePluginMountData(extraPlugins), async () => {
for (const plugin of extraPlugins) {
await awaitRecipe(`extra-plugin.mount:${plugin.slug}`, runtime.mount({
type: "directory",
source: plugin.source,
target: plugin.target,
mode: "readonly",
metadata: {
kind: "extra-plugin",
slug: plugin.slug,
source: plugin.provenance,
},
}))
for (const [index, plugin] of extraPlugins.entries()) {
await awaitRecipe(`extra-plugin.mount:${plugin.slug}`, runtime.mount(extraPluginMounts[index]))
interruption?.throwIfInterrupted()
}
})
Expand Down Expand Up @@ -190,6 +193,7 @@ export async function applyRecipeRuntimeSetup(args: {
}

const materializableMounts: MountSpec[] = [
...extraPluginMounts,
...inputMounts,
...stagedFiles.map((stagedFile) => ({
type: stagedFile.type,
Expand All @@ -205,17 +209,19 @@ export async function applyRecipeRuntimeSetup(args: {
interruption?.throwIfInterrupted()
}

const muPluginInstallCode = installMuPluginsCode(extraPlugins)
const isolateManagedMultisitePreinstall = recipeHasManagedMysqlMultisitePhpunit(recipe, runtimeSpec)
const muPluginInstallCode = isolateManagedMultisitePreinstall ? null : installMuPluginsCode(extraPlugins)
if (muPluginInstallCode) {
executions.push(withRecipeExecutionPhase(await runtime.execute({ command: "wordpress.run-php", args: setupPhpArgs(muPluginInstallCode) }), "setup", -2, "extra-plugin.install-mu-loader"))
}

const composerAutoloaderInstallCode = installPluginComposerAutoloadersCode(extraPlugins)
const composerAutoloaderInstallCode = isolateManagedMultisitePreinstall ? null : installPluginComposerAutoloadersCode(extraPlugins)
if (composerAutoloaderInstallCode) {
executions.push(withRecipeExecutionPhase(await runtime.execute({ command: "wordpress.run-php", args: setupPhpArgs(composerAutoloaderInstallCode) }), "setup", -2, "extra-plugin.install-composer-autoloaders"))
}

const activatedPlugins = extraPlugins.filter((plugin) => plugin.loadAs === "plugin" && plugin.activate !== false)
const deferredPluginFiles = managedPhpunitDeferredPluginFiles(recipe, runtimeSpec)
const activatedPlugins = extraPlugins.filter((plugin) => plugin.loadAs === "plugin" && plugin.activate !== false && !deferredPluginFiles.has(plugin.pluginFile))
if (activatedPlugins.length > 0) {
const activePluginsAfterActivation = await phaseTracker.run("activate_plugins", phasePluginActivationData(activatedPlugins), async () => {
for (const plugin of activatedPlugins) {
Expand Down Expand Up @@ -243,6 +249,28 @@ export async function applyRecipeRuntimeSetup(args: {
return { executions }
}

function managedPhpunitDeferredPluginFiles(recipe: WorkspaceRecipe, runtimeSpec: Pick<RuntimeCreateSpec, "environment" | "runtimeEnv">): Set<string> {
const deferred = new Set<string>()
for (const step of [...(recipe.workflow.before ?? []), ...recipe.workflow.steps, ...(recipe.workflow.after ?? [])]) {
if (step.command !== "wordpress.phpunit" || !requiresManagedMysqlMultisitePreinstall(step.args ?? [], runtimeSpec)) continue
const raw = (step.args ?? []).find((arg) => arg.startsWith("dependency-plugins-json="))?.slice("dependency-plugins-json=".length)
if (!raw) continue
const plugins = JSON.parse(raw) as unknown
if (!Array.isArray(plugins)) throw new Error("dependency-plugins-json must be a JSON array")
for (const plugin of plugins) {
if (plugin && typeof plugin === "object" && !Array.isArray(plugin) && typeof (plugin as { pluginFile?: unknown }).pluginFile === "string") {
deferred.add((plugin as { pluginFile: string }).pluginFile)
}
}
}
return deferred
}

function recipeHasManagedMysqlMultisitePhpunit(recipe: WorkspaceRecipe, runtimeSpec: Pick<RuntimeCreateSpec, "environment" | "runtimeEnv">): boolean {
return [...(recipe.workflow.before ?? []), ...recipe.workflow.steps, ...(recipe.workflow.after ?? [])]
.some((step) => step.command === "wordpress.phpunit" && requiresManagedMysqlMultisitePreinstall(step.args ?? [], runtimeSpec))
}

export async function cleanupInputMountBaselines(paths: string[]): Promise<void> {
await Promise.all(paths.map((path) => rm(path, { recursive: true, force: true })))
paths.length = 0
Expand Down
23 changes: 22 additions & 1 deletion packages/runtime-core/src/recipe-builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { buildRuntimePackageRunRecipe, CODEBOX_RUN_RUNTIME_PACKAGE_ABILITY, RUNT
export { RUNTIME_PACKAGE_DIAGNOSTIC_SCHEMA, RUNTIME_PACKAGE_RESULT_SCHEMA, RUNTIME_PACKAGE_TASK_SCHEMA, normalizeRuntimePackageResult, normalizeRuntimePackageTask, validateRuntimePackageTask, type RuntimePackageDiagnostic, type RuntimePackageResult, type RuntimePackageTask } from "./runtime-package-contracts.js"
import { normalizeSharedMounts } from "./mount-primitives.js"
import { DEFAULT_WORDPRESS_VERSION } from "./runtime-defaults.js"
import { resolvePluginEntrypointContract } from "./component-contracts.js"

type JsonObject = Record<string, unknown>

Expand Down Expand Up @@ -76,6 +77,7 @@ export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptio
const pluginTarget = `/wordpress/wp-content/plugins/${pluginSlug}`
const autoloadFile = options.autoloadFile ?? (options.bootstrapMode === "project" ? "" : "/wp-codebox-vendor/autoload.php")
const services = phpunitRuntimeServices(options.databaseType, options.services)
const extraPlugins = normalizeExtraPlugins(options.extra_plugins)

return {
schema: "wp-codebox/workspace-recipe/v1",
Expand All @@ -90,7 +92,7 @@ export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptio
...(options.backendPackage ? { backendPackage: options.backendPackage } : {}),
},
inputs: {
extra_plugins: normalizeExtraPlugins(options.extra_plugins),
extra_plugins: extraPlugins,
...(services.length > 0 ? { services } : {}),
mounts: normalizeRecipeMounts([
...(options.pluginSource ? [{ source: options.pluginSource, target: pluginTarget } satisfies WorkspaceRecipeMount] : []),
Expand All @@ -116,6 +118,7 @@ export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptio
commandArg("phpunit-xml", options.phpunitXml ?? `${pluginTarget}/phpunit.xml.dist`),
commandArg("phpunit-xml-default", options.phpunitXml === undefined ? "1" : ""),
commandStringListArg("dependency-mounts", options.dependencyMounts ?? []),
commandJsonArg("dependency-plugins-json", phpunitDependencyPlugins(options.dependencyMounts ?? [], extraPlugins)),
commandJsonArg("bootstrap-files-json", options.bootstrapFiles ?? []),
commandJsonArg("preload-files-json", options.preloadFiles ?? []),
commandJsonArg("phpunit-args-json", options.phpunitArgs ?? []),
Expand All @@ -129,6 +132,24 @@ export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptio
}
}

function phpunitDependencyPlugins(mounts: readonly string[], plugins: readonly WorkspaceRecipeExtraPlugin[]): Array<{ path: string; pluginFile: string; activate: boolean; loadAs: "plugin" | "mu-plugin" }> {
return mounts.map((path) => {
const normalized = path.replace(/\/+$/g, "")
const slug = normalized.slice(normalized.lastIndexOf("/") + 1)
const plugin = plugins.find((candidate) => candidate.slug === slug || candidate.mountSlug === slug)
const loadAs = plugin?.loadAs === "mu-plugin" ? "mu-plugin" : "plugin"
const pluginFile = plugin
? resolvePluginEntrypointContract({ source: plugin.source ?? plugin.sourcePath ?? "", slug, pluginFile: plugin.pluginFile, loadAs }).pluginFile
: `${slug}/${slug}.php`
return {
path: loadAs === "mu-plugin" ? `/wordpress/wp-content/mu-plugins/contained-runtime/${slug}` : path,
pluginFile,
activate: plugin?.activate !== false,
loadAs,
}
})
}

function phpunitRuntimeServices(databaseType: WordPressPhpunitRecipeOptions["databaseType"], services: WorkspaceRecipeRuntimeService[] = []): WorkspaceRecipeRuntimeService[] {
if (databaseType !== undefined && databaseType !== "sqlite" && databaseType !== "mysql") {
throw new Error(`Unsupported PHPUnit database type: ${databaseType}`)
Expand Down
1 change: 1 addition & 0 deletions packages/runtime-playground/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export { PLAYWRIGHT_CLOCK_CONTROL_CAPABILITIES, createBrowserClockController, ty
export { PLAYWRIGHT_BROWSER_ENVIRONMENT_CAPABILITIES, applyPlaywrightPageEnvironment, browserEnvironmentCell, createPlaywrightBrowserEnvironmentContext, observePlaywrightBrowserEnvironment, resolvePlaywrightBrowserEnvironment, runPlaywrightBrowserEnvironmentMatrix, type BrowserEnvironmentCpuProfile, type BrowserEnvironmentNetworkProfile, type PlaywrightBrowserEnvironmentExecutionInput, type PlaywrightBrowserEnvironmentOptions, type PlaywrightBrowserEnvironmentRuntime, type PlaywrightBrowserEnvironmentSession } from "./browser-environment-matrix.js"
export { WORDPRESS_ADVERSARIAL_ADAPTER_SCHEMA, WORDPRESS_ADVERSARIAL_CAPABILITIES, WORDPRESS_ADVERSARIAL_ORACLES, WORDPRESS_CLOCK_CONTROL_CAPABILITIES, WORDPRESS_HTTP_TRANSPORT_FAULT_CAPABILITIES, createWordPressAdversarialAdapter, evaluateWordPressAdversarialOracles, negotiateWordPressHttpTransportFaults, wordpressAdversarialActionSpec, wordpressHttpFaultConfigurationAction, wordpressNoveltySignals, wordpressSchedulerClockAction, type WordPressAdapterFidelity, type WordPressAdversarialAction, type WordPressAdversarialAdapter, type WordPressAdversarialCapability, type WordPressAdversarialSurface } from "./wordpress-adversarial-adapter.js"
export { normalizePreviewReviewerAccess, previewReviewerAccess } from "./preview-reviewer-access.js"
export { phpunitExecutionSemantics, requiresManagedMysqlMultisitePreinstall, type PhpunitExecutionSemantics } from "./phpunit-command-semantics.js"
export { applyVfsMountSnapshots, materializePlaygroundMountsFromVfs, materializePlaygroundStagedInputs, type HostMountSnapshot, type MountMaterializationResult, type StagedInputMaterializationResult, type VfsMountSnapshot } from "./mount-materialization.js"
export { buildReplayExportBlueprint, buildReplayableWordPressSiteBlueprint, buildReplayableWordPressSiteLimitations, writeReplayExportPackage, writeReplayableWordPressSiteBundle, type ReplayExportPackage, type ReplayExportPackageOptions, type ReplayableWordPressSiteBundle, type ReplayableWordPressSiteBundleManifest, type ReplayableWordPressSiteBundleOptions } from "./replayable-wordpress-site-bundle.js"
export { RUNTIME_CAPTURE_STATUS_SCHEMA, runtimeCaptureStatus, type RuntimeCaptureDiagnostic, type RuntimeCaptureState, type RuntimeCaptureStatus, type RuntimeCaptureStatusInput } from "./runtime-capture-status.js"
Expand Down
Loading
Loading