Skip to content

Commit 55863c2

Browse files
authored
Merge pull request #856 from Automattic/fix/issues-851-854-recipe-evidence
Fix recipe evidence completion contracts
2 parents 6de9042 + e82fd89 commit 55863c2

13 files changed

Lines changed: 343 additions & 20 deletions

docs/recipe-contract.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,34 @@ come from the command catalog:
138138
npm run wp-codebox -- commands --json
139139
```
140140

141+
Use `allowFailure: true` or `advisory: true` for evidence-only workflow steps.
142+
Failed advisory steps are reported in `advisoryFailures` and do not make an
143+
otherwise successful recipe return `success: false`.
144+
145+
```json
146+
{
147+
"command": "wordpress.browser-actions",
148+
"args": ["url=/", "steps-json=[...]"],
149+
"advisory": true
150+
}
151+
```
152+
153+
## Recipe Output Evidence
154+
155+
`recipe-run --json` returns `wp-codebox/recipe-run/v1`. Browser command sidecars
156+
are promoted into `browserEvidence` so callers can discover stable evidence
157+
without hardcoding artifact paths or parsing command stdout.
158+
159+
Each `browserEvidence` entry includes the workflow phase/index, command,
160+
summary file, artifact file refs, summary payload, and `scriptResult` when the
161+
browser command produced one. The same browser evidence is mirrored into
162+
`latest-runtime.json` under the run artifact pointer.
163+
164+
`--preview-hold <duration>` records held-preview lifecycle metadata in the
165+
artifact bundle and returns after recipe work finishes. Use
166+
`--preview-hold-blocking` only for operator workflows that need the CLI process
167+
to keep a live preview server open for the hold duration.
168+
141169
## WordPress PHPUnit Runtime
142170

143171
`wordpress.phpunit` is the lightweight WP Codebox equivalent of plugin PHPUnit

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mkdir, writeFile } from "node:fs/promises"
22
import { join, relative } from "node:path"
33
import { stripUndefined, type ArtifactBundle, type RuntimeInfo } from "@automattic/wp-codebox-core"
44
import type { RunOutput } from "../runtime-command-wrappers.js"
5-
import type { RecipeArtifactPointerCommandStatus, RecipeArtifactPointerState, RecipePhaseEvidence } from "./recipe-run-types.js"
5+
import type { RecipeArtifactPointerCommandStatus, RecipeArtifactPointerState, RecipeBrowserEvidence, RecipePhaseEvidence } from "./recipe-run-types.js"
66

77
export class RecipeArtifactPointerTracker {
88
private command: string | undefined
@@ -11,6 +11,7 @@ export class RecipeArtifactPointerTracker {
1111
private artifacts: ArtifactBundle | undefined
1212
private failure: RunOutput["error"] | undefined
1313
private phases: RecipePhaseEvidence[] = []
14+
private browserEvidence: RecipeBrowserEvidence[] = []
1415

1516
constructor(private readonly directory: string | undefined, private readonly runId: string, private readonly recipePath: string, private readonly startedAt: string) {}
1617

@@ -23,8 +24,9 @@ export class RecipeArtifactPointerTracker {
2324
this.commandStatus = state.commandStatus ?? this.commandStatus
2425
this.runtime = state.runtime ?? this.runtime
2526
this.artifacts = state.artifacts ?? this.artifacts
26-
this.failure = state.failure ?? this.failure
27+
this.failure = state.failure ?? (state.commandStatus === "completed" || state.commandStatus === "running" ? undefined : this.failure)
2728
this.phases = state.phases ?? this.phases
29+
this.browserEvidence = state.browserEvidence ?? this.browserEvidence
2830

2931
const pointer = stripUndefined({
3032
schema: "wp-codebox/recipe-run-artifact-pointer/v1",
@@ -39,6 +41,7 @@ export class RecipeArtifactPointerTracker {
3941
commandStatus: this.commandStatus,
4042
failure: this.failure,
4143
failurePhase: recipeArtifactPointerFailurePhase(this.failure, this.phases),
44+
browserEvidence: this.browserEvidence.length > 0 ? this.browserEvidence : undefined,
4245
paths: recipeArtifactPointerPaths(this.directory, this.runtime, this.artifacts),
4346
})
4447

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ export function createRecipeInterruptionController(): RecipeInterruptionControll
130130
controller.dispose()
131131
process.kill(process.pid, metadata.signal)
132132
},
133+
clear() {
134+
metadata = undefined
135+
},
133136
}
134137

135138
return controller

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class RecipeDeclaredArtifactFailureError extends Error {
5151

5252
export function exitAfterPlaygroundCliBootFailure(output: RecipeRunCommandOutput): void {
5353
if (output.schema === "wp-codebox/recipe-run/v1" && hasSerializedErrorCode(output.error, "wp-codebox-playground-cli-exited")) {
54-
process.exit(output.success ? 0 : 1)
54+
process.exitCode = output.success ? 0 : 1
5555
}
5656
}
5757

@@ -74,10 +74,26 @@ function hasSerializedErrorCode(error: RunOutput["error"] | undefined, code: str
7474

7575
export function exitAfterRecipeRunTimeout(output: RecipeRunCommandOutput): void {
7676
if (output.schema === "wp-codebox/recipe-run/v1" && output.error?.code === "recipe-run-timeout") {
77-
process.exit(output.success ? 0 : 1)
77+
process.exitCode = output.success ? 0 : 1
7878
}
7979
}
8080

81+
export async function writeRecipeJsonOutput(output: unknown): Promise<void> {
82+
await writeStdout(`${JSON.stringify(output, null, 2)}\n`)
83+
}
84+
85+
function writeStdout(contents: string): Promise<void> {
86+
return new Promise((resolve, reject) => {
87+
process.stdout.write(contents, (error) => {
88+
if (error) {
89+
reject(error)
90+
return
91+
}
92+
resolve()
93+
})
94+
})
95+
}
96+
8197
export function printJsonFailureDiagnostic(output: { success: boolean; error?: { message?: string }; logs?: string[] }): void {
8298
if (output.success) {
8399
return

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface RecipeRunOptions {
1212
previewPublicUrl?: string
1313
previewPort?: number
1414
previewBind?: string
15+
previewHoldBlocking: boolean
1516
timeoutMs: number
1617
json: boolean
1718
dryRun: boolean
@@ -50,6 +51,8 @@ export interface RecipeRunOutput {
5051
probes?: RecipeRunProbe[]
5152
declaredArtifacts?: RecipeRunDeclaredArtifact[]
5253
phaseEvidence?: RecipePhaseEvidence[]
54+
advisoryFailures?: RecipeAdvisoryFailure[]
55+
browserEvidence?: RecipeBrowserEvidence[]
5356
diagnostics?: RecipeRuntimeDiagnostic[]
5457
validation?: {
5558
issues: RecipeValidationIssue[]
@@ -72,6 +75,37 @@ export type RecipeExecutionResult = ExecutionResult & {
7275
recipePhase?: RecipeWorkflowPhase
7376
recipeStepIndex?: number
7477
recipeCommand?: string
78+
recipeAdvisory?: boolean
79+
}
80+
81+
export interface RecipeAdvisoryFailure {
82+
schema: "wp-codebox/recipe-advisory-failure/v1"
83+
phase: RecipeWorkflowPhase
84+
index: number
85+
command: string
86+
status: "failed"
87+
error: RunOutput["error"]
88+
}
89+
90+
export interface RecipeBrowserEvidenceFileRef {
91+
path: string
92+
kind?: string
93+
contentType?: string
94+
sha256?: { algorithm: "sha256"; value: string }
95+
}
96+
97+
export interface RecipeBrowserEvidence {
98+
schema: "wp-codebox/recipe-browser-evidence/v1"
99+
phase?: RecipeWorkflowPhase
100+
index?: number
101+
command: string
102+
status: "completed" | "failed"
103+
requestedUrl?: string
104+
finalUrl?: string
105+
summaryFile?: RecipeBrowserEvidenceFileRef
106+
files: Record<string, RecipeBrowserEvidenceFileRef | RecipeBrowserEvidenceFileRef[]>
107+
summary?: unknown
108+
scriptResult?: unknown
75109
}
76110

77111
export type RecipeArtifactPointerCommandStatus = "queued" | "running" | "completed" | "failed"
@@ -83,6 +117,7 @@ export interface RecipeArtifactPointerState {
83117
artifacts?: ArtifactBundle
84118
failure?: RunOutput["error"]
85119
phases?: RecipePhaseEvidence[]
120+
browserEvidence?: RecipeBrowserEvidence[]
86121
}
87122

88123
export type RecipePhaseName = "runtime_startup" | "mount_plugins" | "activate_plugins" | "run_blueprint_steps" | "import_fixture_databases" | "run_workloads" | "run_probes" | "collect_artifacts"
@@ -212,4 +247,5 @@ export interface RecipeInterruptionController {
212247
interruptible<T>(promise: Promise<T>): Promise<T>
213248
throwIfInterrupted(): void
214249
propagateIfInterrupted(): void
250+
clear(): void
215251
}

0 commit comments

Comments
 (0)