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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"bench-bootstrap-files-smoke": "tsx scripts/bench-bootstrap-files-smoke.ts",
"wordpress-recipe-builders-smoke": "tsx scripts/wordpress-recipe-builders-smoke.ts",
"recipe-bench-smoke": "tsx scripts/recipe-bench-smoke.ts",
"recipe-build-cli-smoke": "tsx scripts/recipe-build-cli-smoke.ts",
"recipe-browser-bench-metrics-smoke": "tsx scripts/recipe-browser-bench-metrics-smoke.ts",
"recipe-browser-smoke": "tsx scripts/recipe-browser-smoke.ts",
"headless-browser-agent-recipe-smoke": "tsx scripts/headless-browser-agent-recipe-smoke.ts",
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/src/cli-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { routeCliCommand } from "./command-router.js"
import { runArtifactsBrowserMetricsCommand, runArtifactsVerifyCommand } from "./commands/artifacts.js"
import { runCommandsCommand, runRecipeSchemaCommand } from "./commands/discovery.js"
import { runCleanupCommand, runDoctorCommand } from "./commands/doctor.js"
import { runRecipeBuildCommand } from "./commands/recipe-build.js"
import { runRecipeRunCommand, runRecipeValidateCommand } from "./commands/recipe-run.js"
import { runBootCommand, runRunCommand, runValidateBlueprintCommand } from "./commands/runtime.js"
import { runRunsArtifactsCommand, runRunsStatusCommand } from "./commands/runs.js"
import { runWorkspacePolicyCheckCommand } from "./commands/workspace-policy.js"
import { printHelp } from "./output.js"

export async function runCli(args: string[]): Promise<number> {
return routeCliCommand(args, {
printHelp,
boot: runBootCommand,
validateBlueprint: runValidateBlueprintCommand,
recipeRun: runRecipeRunCommand,
recipeValidate: runRecipeValidateCommand,
recipeBuild: runRecipeBuildCommand,
workspacePolicyCheck: runWorkspacePolicyCheckCommand,
artifactsVerify: runArtifactsVerifyCommand,
artifactsBrowserMetrics: runArtifactsBrowserMetricsCommand,
runsStatus: runRunsStatusCommand,
runsArtifacts: runRunsArtifactsCommand,
commands: runCommandsCommand,
recipeSchema: runRecipeSchemaCommand,
doctor: runDoctorCommand,
cleanup: runCleanupCommand,
run: runRunCommand,
})
}
10 changes: 8 additions & 2 deletions packages/cli/src/command-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface CliCommandRouter {
boot: CliCommandHandler
validateBlueprint: CliCommandHandler
recipeValidate: CliCommandHandler
recipeBuild: CliCommandHandler
recipeRun: CliCommandHandler
workspacePolicyCheck: CliCommandHandler
artifactsVerify: CliCommandHandler
Expand Down Expand Up @@ -43,12 +44,17 @@ export async function routeCliCommand(argv: string[], router: CliCommandRouter):
return router.recipeRun(args)
case "recipe": {
const subcommand = args.shift()
if (subcommand !== "validate") {
if (subcommand === "validate") {
return router.recipeValidate(args)
}
if (subcommand === "build") {
return router.recipeBuild(args)
}
{
console.error(`Unknown recipe command: ${subcommand ?? ""}`)
router.printHelp()
return 1
}
return router.recipeValidate(args)
}
case "workspace-policy": {
const subcommand = args.shift()
Expand Down
100 changes: 100 additions & 0 deletions packages/cli/src/commands/recipe-build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { readFile, writeFile } from "node:fs/promises"
import { buildWordPressPhpunitRecipe, type WorkspaceRecipe, type WorkspaceRecipeMount } from "@chubes4/wp-codebox-core"

interface RecipeBuildOptions {
recipeType: "phpunit"
optionsPath: string
outputPath?: string
}

interface WordPressPhpunitBuilderOptions {
wordpressVersion?: string
mounts?: WorkspaceRecipeMount[]
pluginSlug: string
selectedTestFile?: string
changedTestFiles?: string[]
env?: Record<string, unknown>
wpConfigDefines?: Record<string, unknown>
autoloadFile?: string
testsDir?: string
dependencyMounts?: string[]
multisite?: boolean
}

export async function runRecipeBuildCommand(args: string[]): Promise<number> {
const options = parseRecipeBuildOptions(args)
const builderOptions = JSON.parse(await readFile(options.optionsPath, "utf8")) as WordPressPhpunitBuilderOptions
const recipe = buildRecipe(options.recipeType, builderOptions)
const json = `${JSON.stringify(recipe, null, 2)}\n`

if (options.outputPath) {
await writeFile(options.outputPath, json)
} else {
process.stdout.write(json)
}

return 0
}

function buildRecipe(recipeType: RecipeBuildOptions["recipeType"], options: WordPressPhpunitBuilderOptions): WorkspaceRecipe {
switch (recipeType) {
case "phpunit":
return buildWordPressPhpunitRecipe({
wordpressVersion: stringOrUndefined(options.wordpressVersion),
mounts: Array.isArray(options.mounts) ? options.mounts : [],
pluginSlug: requiredString(options.pluginSlug, "pluginSlug"),
selectedTestFile: stringOrUndefined(options.selectedTestFile),
changedTestFiles: Array.isArray(options.changedTestFiles) ? options.changedTestFiles : [],
env: plainObject(options.env),
wpConfigDefines: plainObject(options.wpConfigDefines),
autoloadFile: stringOrUndefined(options.autoloadFile),
testsDir: stringOrUndefined(options.testsDir),
dependencyMounts: Array.isArray(options.dependencyMounts) ? options.dependencyMounts : [],
multisite: Boolean(options.multisite),
})
}
}

function parseRecipeBuildOptions(args: string[]): RecipeBuildOptions {
const recipeType = args.shift()
if (recipeType !== "phpunit") {
throw new Error(`Unknown recipe build type: ${recipeType ?? ""}`)
}

let optionsPath = ""
let outputPath: string | undefined
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]
switch (arg) {
case "--options":
optionsPath = args[++index] ?? ""
break
case "--output":
outputPath = args[++index] ?? ""
break
default:
throw new Error(`Unknown recipe build option: ${arg}`)
}
}

if (!optionsPath) {
throw new Error("recipe build phpunit requires --options <path>")
}

return { recipeType, optionsPath, outputPath }
}

function requiredString(value: unknown, name: string): string {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(`Recipe build option ${name} must be a non-empty string`)
}
return value
}

function stringOrUndefined(value: unknown): string | undefined {
return typeof value === "string" && value !== "" ? value : undefined
}

function plainObject(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {}
}
31 changes: 2 additions & 29 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,6 @@
#!/usr/bin/env node
import { routeCliCommand } from "./command-router.js"
import { runArtifactsBrowserMetricsCommand, runArtifactsVerifyCommand } from "./commands/artifacts.js"
import { runCommandsCommand, runRecipeSchemaCommand } from "./commands/discovery.js"
import { runCleanupCommand, runDoctorCommand } from "./commands/doctor.js"
import { runRecipeRunCommand, runRecipeValidateCommand } from "./commands/recipe-run.js"
import { runBootCommand, runRunCommand, runValidateBlueprintCommand } from "./commands/runtime.js"
import { runRunsArtifactsCommand, runRunsStatusCommand } from "./commands/runs.js"
import { runWorkspacePolicyCheckCommand } from "./commands/workspace-policy.js"
import { printHelp, serializeError } from "./output.js"

async function runCli(args: string[]): Promise<number> {
return routeCliCommand(args, {
printHelp,
boot: runBootCommand,
validateBlueprint: runValidateBlueprintCommand,
recipeRun: runRecipeRunCommand,
recipeValidate: runRecipeValidateCommand,
workspacePolicyCheck: runWorkspacePolicyCheckCommand,
artifactsVerify: runArtifactsVerifyCommand,
artifactsBrowserMetrics: runArtifactsBrowserMetricsCommand,
runsStatus: runRunsStatusCommand,
runsArtifacts: runRunsArtifactsCommand,
commands: runCommandsCommand,
recipeSchema: runRecipeSchemaCommand,
doctor: runDoctorCommand,
cleanup: runCleanupCommand,
run: runRunCommand,
})
}
import { runCli } from "./cli-entry.js"
import { serializeError } from "./output.js"

runCli(process.argv.slice(2)).then(
(code) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export function printHelp(): void {
wp-codebox doctor [--json] [--fix] [--archive-root <dir>] [--stale-after-seconds <n>]
wp-codebox cleanup [--json] [--archive-root <dir>] [--stale-after-seconds <n>]
wp-codebox workspace-policy check --workspace-root <path> --writable-root <path> [options]
wp-codebox recipe build phpunit --options <path> [--output <path>]
wp-codebox recipe validate --recipe <path> [--json]
wp-codebox artifacts verify --bundle <dir> [--json]
wp-codebox artifacts browser-metrics --bundle <dir> [--json]
Expand All @@ -255,6 +256,8 @@ export function printHelp(): void {

Options:
--recipe <path> Workspace recipe JSON file for recipe-run or recipe validate.
--options <path> Recipe builder options JSON file for recipe build.
--output <path> Optional output JSON path for recipe build; defaults to stdout.
--bundle <dir> Artifact bundle directory for artifacts verify.
--artifacts <dir> Artifact root directory. Also accepted by artifacts verify.
--run-registry <dir>
Expand Down
45 changes: 45 additions & 0 deletions scripts/recipe-build-cli-smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import assert from "node:assert/strict"
import { mkdtemp, readFile, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { runCli } from "../packages/cli/src/cli-entry.js"

const directory = await mkdtemp(join(tmpdir(), "wp-codebox-recipe-build-cli-"))
const optionsPath = join(directory, "phpunit-options.json")
const outputPath = join(directory, "phpunit-recipe.json")

await writeFile(optionsPath, JSON.stringify({
wordpressVersion: "6.10",
mounts: [{ source: "/repo/plugin", target: "/wordpress/wp-content/plugins/demo" }],
pluginSlug: "demo",
selectedTestFile: "tests/DemoTest.php",
changedTestFiles: ["tests/DemoTest.php"],
env: { HOMEBOY_FLAG: "yes" },
wpConfigDefines: { WP_DEBUG: true },
autoloadFile: "/wp-codebox-vendor/autoload.php",
testsDir: "/wp-codebox-vendor/wp-phpunit/wp-phpunit",
dependencyMounts: ["/wordpress/wp-content/plugins/dep"],
multisite: true,
}, null, 2))

const exitCode = await runCli(["recipe", "build", "phpunit", "--options", optionsPath, "--output", outputPath])
assert.equal(exitCode, 0)

const recipe = JSON.parse(await readFile(outputPath, "utf8"))
assert.equal(recipe.schema, "wp-codebox/workspace-recipe/v1")
assert.equal(recipe.runtime.wp, "6.10")
assert.equal(recipe.workflow.steps[0].command, "wordpress.phpunit")
assert.deepEqual(recipe.inputs.mounts, [{ source: "/repo/plugin", target: "/wordpress/wp-content/plugins/demo", mode: "readwrite" }])
assert.deepEqual(recipe.workflow.steps[0].args, [
"plugin-slug=demo",
"test-file=tests/DemoTest.php",
'changed-tests-json=["tests/DemoTest.php"]',
'env-json={"HOMEBOY_FLAG":"yes"}',
'wp-config-defines-json={"WP_DEBUG":true}',
"autoload-file=/wp-codebox-vendor/autoload.php",
"tests-dir=/wp-codebox-vendor/wp-phpunit/wp-phpunit",
"dependency-mounts=/wordpress/wp-content/plugins/dep",
"multisite=1",
])

console.log("recipe build cli smoke passed")