Skip to content

Commit c2caf9a

Browse files
authored
Merge pull request #2107 from Automattic/fix-2056-managed-multisite-composition
2 parents 2563cae + ca9dee5 commit c2caf9a

12 files changed

Lines changed: 558 additions & 98 deletions

packages/cli/src/commands/recipe-run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ export async function runRecipe(options: RecipeRunOptions, interruption?: Recipe
296296
})
297297
interruption?.throwIfInterrupted()
298298

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

301301
fixtureDatabases = await phaseTracker.run("import_fixture_databases", phaseFixtureDatabaseData(recipe), async () => await awaitRecipe("fixture-databases.import", importRecipeFixtureDatabases(recipe, recipeDirectory, runtime!, executions)))
302302
const siteSeeds = await awaitRecipe("site-seeds.import", importRecipeSiteSeeds(recipe, recipeDirectory, runtime!, executions))

packages/cli/src/commands/recipe-runtime-setup.ts

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { cp, mkdtemp, rm, stat } from "node:fs/promises"
22
import { tmpdir } from "node:os"
33
import { join, posix, resolve } from "node:path"
4-
import { phpRuntimeRecipePluginPreloadFunction, type ExecutionResult, type MountSpec, type Runtime, type WorkspaceRecipe, type WorkspaceRecipeMount, type WorkspaceRecipePluginRuntimeHealthProbe } from "@automattic/wp-codebox-core"
4+
import { phpRuntimeRecipePluginPreloadFunction, type ExecutionResult, type MountSpec, type Runtime, type RuntimeCreateSpec, type WorkspaceRecipe, type WorkspaceRecipeMount, type WorkspaceRecipePluginRuntimeHealthProbe } from "@automattic/wp-codebox-core"
5+
import { requiresManagedMysqlMultisitePreinstall } from "@automattic/wp-codebox-playground"
56
import { installMuPluginsCode, installPluginComposerAutoloadersCode, prepareRecipeDependencyOverlays, prepareRecipeExtraPlugins, prepareRecipeRuntimeOverlays, prepareRecipeStagedFiles, prepareRecipeWorkspacePreloads, prepareRecipeWorkspaces, recipeMountType, type PreparedDependencyOverlay, type PreparedExtraPlugin, type PreparedRuntimeOverlay, type PreparedStagedFile, type PreparedWorkspaceMount } from "../recipe-sources.js"
67
import { pluginRuntimeHealthProbeStep, type RecipeWorkflowPhase } from "../recipe-validation.js"
78
import { pluginRuntimeHealthProbeStepIndex, pluginRuntimeSetupStepIndex } from "../recipe-dry-run.js"
@@ -52,9 +53,10 @@ export async function applyRecipeRuntimeSetup(args: {
5253
runtime: Runtime
5354
prepared: PreparedRecipeRuntimeSetup
5455
phaseExecutor: RecipeRunPhaseExecutor
56+
runtimeSpec: Pick<RuntimeCreateSpec, "environment" | "runtimeEnv">
5557
interruption?: RecipeInterruptionController
5658
}): Promise<RecipeRuntimeSetupResult> {
57-
const { recipe, recipeDirectory, runtime, prepared, phaseExecutor, interruption } = args
59+
const { recipe, recipeDirectory, runtime, runtimeSpec, prepared, phaseExecutor, interruption } = args
5860
const { workspaceMounts, extraPlugins, dependencyOverlays, stagedFiles, overlays, inputMountBaselinePaths, inputMountPathMap } = prepared
5961
const executions: RecipeExecutionResult[] = []
6062
const phaseTracker = phaseExecutor.tracker
@@ -127,19 +129,20 @@ export async function applyRecipeRuntimeSetup(args: {
127129
interruption?.throwIfInterrupted()
128130
}
129131

132+
const extraPluginMounts: MountSpec[] = extraPlugins.map((plugin) => ({
133+
type: "directory",
134+
source: plugin.source,
135+
target: plugin.target,
136+
mode: "readonly",
137+
metadata: {
138+
kind: "extra-plugin",
139+
slug: plugin.slug,
140+
source: plugin.provenance,
141+
},
142+
}))
130143
await phaseTracker.run("mount_plugins", phasePluginMountData(extraPlugins), async () => {
131-
for (const plugin of extraPlugins) {
132-
await awaitRecipe(`extra-plugin.mount:${plugin.slug}`, runtime.mount({
133-
type: "directory",
134-
source: plugin.source,
135-
target: plugin.target,
136-
mode: "readonly",
137-
metadata: {
138-
kind: "extra-plugin",
139-
slug: plugin.slug,
140-
source: plugin.provenance,
141-
},
142-
}))
144+
for (const [index, plugin] of extraPlugins.entries()) {
145+
await awaitRecipe(`extra-plugin.mount:${plugin.slug}`, runtime.mount(extraPluginMounts[index]))
143146
interruption?.throwIfInterrupted()
144147
}
145148
})
@@ -190,6 +193,7 @@ export async function applyRecipeRuntimeSetup(args: {
190193
}
191194

192195
const materializableMounts: MountSpec[] = [
196+
...extraPluginMounts,
193197
...inputMounts,
194198
...stagedFiles.map((stagedFile) => ({
195199
type: stagedFile.type,
@@ -205,17 +209,19 @@ export async function applyRecipeRuntimeSetup(args: {
205209
interruption?.throwIfInterrupted()
206210
}
207211

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

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

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

252+
function managedPhpunitDeferredPluginFiles(recipe: WorkspaceRecipe, runtimeSpec: Pick<RuntimeCreateSpec, "environment" | "runtimeEnv">): Set<string> {
253+
const deferred = new Set<string>()
254+
for (const step of [...(recipe.workflow.before ?? []), ...recipe.workflow.steps, ...(recipe.workflow.after ?? [])]) {
255+
if (step.command !== "wordpress.phpunit" || !requiresManagedMysqlMultisitePreinstall(step.args ?? [], runtimeSpec)) continue
256+
const raw = (step.args ?? []).find((arg) => arg.startsWith("dependency-plugins-json="))?.slice("dependency-plugins-json=".length)
257+
if (!raw) continue
258+
const plugins = JSON.parse(raw) as unknown
259+
if (!Array.isArray(plugins)) throw new Error("dependency-plugins-json must be a JSON array")
260+
for (const plugin of plugins) {
261+
if (plugin && typeof plugin === "object" && !Array.isArray(plugin) && typeof (plugin as { pluginFile?: unknown }).pluginFile === "string") {
262+
deferred.add((plugin as { pluginFile: string }).pluginFile)
263+
}
264+
}
265+
}
266+
return deferred
267+
}
268+
269+
function recipeHasManagedMysqlMultisitePhpunit(recipe: WorkspaceRecipe, runtimeSpec: Pick<RuntimeCreateSpec, "environment" | "runtimeEnv">): boolean {
270+
return [...(recipe.workflow.before ?? []), ...recipe.workflow.steps, ...(recipe.workflow.after ?? [])]
271+
.some((step) => step.command === "wordpress.phpunit" && requiresManagedMysqlMultisitePreinstall(step.args ?? [], runtimeSpec))
272+
}
273+
246274
export async function cleanupInputMountBaselines(paths: string[]): Promise<void> {
247275
await Promise.all(paths.map((path) => rm(path, { recursive: true, force: true })))
248276
paths.length = 0

packages/runtime-core/src/recipe-builders.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { buildRuntimePackageRunRecipe, CODEBOX_RUN_RUNTIME_PACKAGE_ABILITY, RUNT
44
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"
55
import { normalizeSharedMounts } from "./mount-primitives.js"
66
import { DEFAULT_WORDPRESS_VERSION } from "./runtime-defaults.js"
7+
import { resolvePluginEntrypointContract } from "./component-contracts.js"
78

89
type JsonObject = Record<string, unknown>
910

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

8082
return {
8183
schema: "wp-codebox/workspace-recipe/v1",
@@ -90,7 +92,7 @@ export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptio
9092
...(options.backendPackage ? { backendPackage: options.backendPackage } : {}),
9193
},
9294
inputs: {
93-
extra_plugins: normalizeExtraPlugins(options.extra_plugins),
95+
extra_plugins: extraPlugins,
9496
...(services.length > 0 ? { services } : {}),
9597
mounts: normalizeRecipeMounts([
9698
...(options.pluginSource ? [{ source: options.pluginSource, target: pluginTarget } satisfies WorkspaceRecipeMount] : []),
@@ -116,6 +118,7 @@ export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptio
116118
commandArg("phpunit-xml", options.phpunitXml ?? `${pluginTarget}/phpunit.xml.dist`),
117119
commandArg("phpunit-xml-default", options.phpunitXml === undefined ? "1" : ""),
118120
commandStringListArg("dependency-mounts", options.dependencyMounts ?? []),
121+
commandJsonArg("dependency-plugins-json", phpunitDependencyPlugins(options.dependencyMounts ?? [], extraPlugins)),
119122
commandJsonArg("bootstrap-files-json", options.bootstrapFiles ?? []),
120123
commandJsonArg("preload-files-json", options.preloadFiles ?? []),
121124
commandJsonArg("phpunit-args-json", options.phpunitArgs ?? []),
@@ -129,6 +132,24 @@ export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptio
129132
}
130133
}
131134

135+
function phpunitDependencyPlugins(mounts: readonly string[], plugins: readonly WorkspaceRecipeExtraPlugin[]): Array<{ path: string; pluginFile: string; activate: boolean; loadAs: "plugin" | "mu-plugin" }> {
136+
return mounts.map((path) => {
137+
const normalized = path.replace(/\/+$/g, "")
138+
const slug = normalized.slice(normalized.lastIndexOf("/") + 1)
139+
const plugin = plugins.find((candidate) => candidate.slug === slug || candidate.mountSlug === slug)
140+
const loadAs = plugin?.loadAs === "mu-plugin" ? "mu-plugin" : "plugin"
141+
const pluginFile = plugin
142+
? resolvePluginEntrypointContract({ source: plugin.source ?? plugin.sourcePath ?? "", slug, pluginFile: plugin.pluginFile, loadAs }).pluginFile
143+
: `${slug}/${slug}.php`
144+
return {
145+
path: loadAs === "mu-plugin" ? `/wordpress/wp-content/mu-plugins/contained-runtime/${slug}` : path,
146+
pluginFile,
147+
activate: plugin?.activate !== false,
148+
loadAs,
149+
}
150+
})
151+
}
152+
132153
function phpunitRuntimeServices(databaseType: WordPressPhpunitRecipeOptions["databaseType"], services: WorkspaceRecipeRuntimeService[] = []): WorkspaceRecipeRuntimeService[] {
133154
if (databaseType !== undefined && databaseType !== "sqlite" && databaseType !== "mysql") {
134155
throw new Error(`Unsupported PHPUnit database type: ${databaseType}`)

packages/runtime-playground/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export { PLAYWRIGHT_CLOCK_CONTROL_CAPABILITIES, createBrowserClockController, ty
2020
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"
2121
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"
2222
export { normalizePreviewReviewerAccess, previewReviewerAccess } from "./preview-reviewer-access.js"
23+
export { phpunitExecutionSemantics, requiresManagedMysqlMultisitePreinstall, type PhpunitExecutionSemantics } from "./phpunit-command-semantics.js"
2324
export { applyVfsMountSnapshots, materializePlaygroundMountsFromVfs, materializePlaygroundStagedInputs, type HostMountSnapshot, type MountMaterializationResult, type StagedInputMaterializationResult, type VfsMountSnapshot } from "./mount-materialization.js"
2425
export { buildReplayExportBlueprint, buildReplayableWordPressSiteBlueprint, buildReplayableWordPressSiteLimitations, writeReplayExportPackage, writeReplayableWordPressSiteBundle, type ReplayExportPackage, type ReplayExportPackageOptions, type ReplayableWordPressSiteBundle, type ReplayableWordPressSiteBundleManifest, type ReplayableWordPressSiteBundleOptions } from "./replayable-wordpress-site-bundle.js"
2526
export { RUNTIME_CAPTURE_STATUS_SCHEMA, runtimeCaptureStatus, type RuntimeCaptureDiagnostic, type RuntimeCaptureState, type RuntimeCaptureStatus, type RuntimeCaptureStatusInput } from "./runtime-capture-status.js"

0 commit comments

Comments
 (0)