@@ -20,7 +20,7 @@ import { previewSpec, releaseRuntime, runtimeMetadata, type RunOutput } from "..
2020import { artifactManifestFilesByPath , parseBenchResults , writeBenchmarkArtifactEvidence } from "./recipe-run-benchmark-artifacts.js"
2121import { createRecipeRunContext } from "./recipe-run-context.js"
2222import { collectRecipeDeclaredArtifacts , materializeTypedRecipeDeclaredArtifacts , recipeDeclaredArtifactFailure , recipeProbeFailure , recipeRuntimeEvidenceFiles } from "./recipe-declared-artifacts.js"
23- import { completedRecipeOutputFields , finalizeCompletedRecipeRun , finalizeRecipeValidationFailure , finalizeRecoveredRecipeFailure , runRecipeCleanup , type RunResourceCleanupEvidence } from "./recipe-run-finalizer.js"
23+ import { completedRecipeOutputFields , finalizeCompletedRecipeRun , finalizeRecipeValidationFailure , finalizeRecoveredRecipeFailure , runRecipeCleanup , RunResourceCleanupError , type RunResourceCleanupEvidence } from "./recipe-run-finalizer.js"
2424import { RecipeRunPhaseExecutor } from "./recipe-run-phase-executor.js"
2525import { RecipeArtifactsMountConflictError , recipeArtifactsMountConflict } from "./recipe-run-artifacts-mount-guard.js"
2626import { createRecipeInterruptionController , interruptedRecipeOutput , markRecipeArtifactsFinalized , recipeInterruptionSerializedError } from "./recipe-run-interruption.js"
@@ -29,6 +29,7 @@ import { RecipePhaseError } from "./recipe-run-phases.js"
2929import { markPreviewLeaseAvailable , markPreviewLeaseFailed , markPreviewLeaseReleased , startPreviewLeaseRecipeRun } from "./preview-lease.js"
3030import { importRecipeSiteSeeds } from "./recipe-site-seeds.js"
3131import { applyRecipeRuntimeSetup , cleanupInputMountBaselines , prepareRecipeRuntimeSetup , recipeRunDependencyOverlay , recipeRunExtraPlugin , recipeRunStagedFile , rewriteInputMountPathArgs } from "./recipe-runtime-setup.js"
32+ import { provisionRuntimeServices , provisionRuntimeServicesForRecipe , runtimeServiceEvidenceFromError , type RuntimeServiceEvidence } from "../runtime-services.js"
3233import { distributionStartupProbeFailure , executeRecipeCollectWorkloadResult , executeRecipeWorkflowStep , recipeAdvisoryFailure , recipeBrowserEvidence , recipeStepFailure , recipeWorkflowArgsEvidence , recipeWorkflowStepIsAdvisory , runDistributionSetupArtifacts , runDistributionStartupProbes , runRecipeProbes , withRecipeExecutionPhase } from "./recipe-run-workflow-evidence.js"
3334import type { RecipeAdvisoryFailure , RecipeBrowserEvidence , RecipeDiagnosticArtifactRef , RecipeEffectiveRecipeArtifact , RecipeExecutionResult , RecipeFuzzCaseCommandRef , RecipeFuzzCaseResult , RecipeFuzzCaseStatus , RecipeFuzzRunResult , RecipeInterruptionController , RecipePhaseEvidence , RecipePhaseName , RecipePhpWasmRuntimeDiagnostic , RecipeRunCommandOutput , RecipeRunComponentContract , RecipeRunDeclaredArtifact , RecipeRunDistributionSetupArtifact , RecipeRunDistributionStartupProbe , RecipeRunFixtureDatabase , RecipeRunOptions , RecipeRunOutput , RecipeRunProbe , RecipeRunProvenance , RecipeRunStagedFile , RecipeRuntimeDiagnostic , RecipeStepFailure , RecipeValidateOptions , RecipeValidateOutput } from "./recipe-run-types.js"
3435
@@ -95,7 +96,7 @@ export async function runRecipeValidateCommand(args: string[]): Promise<number>
9596 return output . success ? 0 : 1
9697}
9798
98- async function runRecipe ( options : RecipeRunOptions , interruption ?: RecipeInterruptionController ) : Promise < RecipeRunOutput > {
99+ export async function runRecipe ( options : RecipeRunOptions , interruption ?: RecipeInterruptionController ) : Promise < RecipeRunOutput > {
99100 const mountConflictFailure = await recipeArtifactsMountConflictFailure ( options )
100101 if ( mountConflictFailure ) {
101102 return mountConflictFailure
@@ -160,6 +161,14 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru
160161 let artifacts : ArtifactBundle | undefined
161162 let startupDurationMs : number | undefined
162163 let cleanupEvidence : RunResourceCleanupEvidence | undefined
164+ let managedServices : Awaited < ReturnType < typeof provisionRuntimeServices > > | undefined
165+ let managedServicesReleased = false
166+ let serviceEvidence : RuntimeServiceEvidence [ ] = [ ]
167+ const releaseManagedServices = async ( ) : Promise < void > => {
168+ if ( ! managedServices || managedServicesReleased ) return
169+ await managedServices . release ( )
170+ managedServicesReleased = true
171+ }
163172 let runtimeDestroyed = false
164173 const destroyActiveRuntime = async ( ) : Promise < void > => {
165174 if ( ! runtime || runtimeDestroyed ) {
@@ -180,6 +189,13 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru
180189 interruption ?. throwIfInterrupted ( )
181190
182191 runRecord = await runRegistry . update ( runRecord . runId , { status : "booting" } )
192+ managedServices = await phaseTracker . run ( "provision_runtime_services" , { services : recipe . inputs ?. services ?. map ( ( { id, kind } ) => ( { id, kind } ) ) ?? [ ] } , async ( ) => await provisionRuntimeServicesForRecipe (
193+ recipe . inputs ?. services ?? [ ] ,
194+ async ( provisioning ) => await awaitRecipe ( "runtime-services.provision" , provisioning ) ,
195+ { signal : interruption ?. signal , onEvidence : ( evidence ) => { serviceEvidence = evidence } } ,
196+ ) )
197+ serviceEvidence = managedServices . evidence
198+ Object . assign ( runtimeEnv , managedServices . env )
183199 const runtimeEnvironment = {
184200 kind : "wordpress" as const ,
185201 name : plan . runtime . name ,
@@ -200,6 +216,7 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru
200216 metadata : {
201217 ...runtimeMetadata ( configuredArtifactsDirectory , plan . runtime . wp ) ,
202218 run : { runId : runRecord . runId , registryDirectory : runRegistry . directory } ,
219+ ...( serviceEvidence . length > 0 ? { managedRuntimeServices : serviceEvidence } : { } ) ,
203220 ...recipeRunMetadata ( recipe , recipePath , workspaceMounts , extraPlugins , dependencyOverlays , stagedFiles , overlays , backendPackage , effectivePreview ) ,
204221 } ,
205222 preview : previewSpec ( effectivePreview . publicUrl , effectivePreview . port , effectivePreview . bind , effectivePreview . siteUrl , effectivePreview . lease ) ,
@@ -346,7 +363,7 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru
346363 await markPreviewLeaseAvailable ( options . previewLeaseFile , { runId : runRecord . runId , preview : artifacts . preview , holdSeconds : options . previewHoldSeconds } )
347364 }
348365 const activeRuntime = runtime
349- cleanupEvidence = await runRecipeCleanup ( runRegistry , runRecord , async ( ) => {
366+ cleanupEvidence = await runManagedServiceCleanup ( runRegistry , runRecord , serviceEvidence , false , async ( ) => {
350367 await awaitRecipe ( "runtime.release" , async ( ) => {
351368 try {
352369 await releaseRuntime ( activeRuntime , successfulRecipe && options . previewHoldBlocking ? options . previewHoldSeconds : 0 , async ( ) => {
@@ -360,6 +377,7 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru
360377 interruption . clear ( )
361378 }
362379 } )
380+ await releaseManagedServices ( )
363381 await cleanupRecipePreparedSources ( workspaceMounts , extraPlugins , stagedFiles , overlays , dependencyOverlays )
364382 await cleanupInputMountBaselines ( inputMountBaselinePaths )
365383 } )
@@ -414,6 +432,11 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru
414432 output : { ...completedRecipeOutputFields ( { executions, componentContracts : componentContractResults ( recipe , extraPlugins , phaseTracker . list ( ) , executions ) , stagedFiles : stagedFiles . map ( recipeRunStagedFile ) , fixtureDatabases, siteSeeds, distributionSetupArtifacts, distributionStartupProbes, probes, declaredArtifacts, stepFailures, phaseEvidence : phaseTracker . list ( ) , advisoryFailures, browserEvidence, benchResultsList, fuzzRun : fuzzRunResult , evidence } ) , provenance : recipeRunProvenance ( recipe , recipePath ) } ,
415433 } )
416434 } catch ( error ) {
435+ const failedServiceEvidence = runtimeServiceEvidenceFromError ( error )
436+ if ( failedServiceEvidence ) {
437+ serviceEvidence = failedServiceEvidence
438+ await runRegistry . update ( runRecord . runId , { metadata : { managedRuntimeServices : serviceEvidence } } )
439+ }
417440 await markPreviewLeaseFailed ( options . previewLeaseFile , error )
418441 const serializedError = interruption ?. metadata ? recipeInterruptionSerializedError ( interruption . metadata ) : serializeRecipeRunError ( error )
419442 const failureDiagnostics = recipeFailureRuntimeEvidenceFile ( {
@@ -493,7 +516,8 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru
493516 ] )
494517 }
495518
496- cleanupEvidence = await runRecipeCleanup ( runRegistry , runRecord , async ( ) => {
519+ cleanupEvidence = await runManagedServiceCleanup ( runRegistry , runRecord , serviceEvidence , true , async ( ) => {
520+ await releaseManagedServices ( )
497521 await cleanupRecipePreparedSources ( workspaceMounts , extraPlugins , stagedFiles , overlays , dependencyOverlays )
498522 await cleanupInputMountBaselines ( inputMountBaselinePaths )
499523 } )
@@ -534,10 +558,35 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru
534558 } ,
535559 } )
536560 } finally {
561+ if ( managedServices && ! managedServicesReleased ) {
562+ await managedServices . release ( ) . catch ( ( ) => undefined )
563+ await runRegistry . update ( runRecord . runId , { metadata : { managedRuntimeServices : managedServices . evidence } } ) . catch ( ( ) => undefined )
564+ }
537565 cancellationWatcher ?. dispose ( )
538566 }
539567}
540568
569+ export async function runManagedServiceCleanup (
570+ runRegistry : RuntimeRunRegistry ,
571+ runRecord : Awaited < ReturnType < RuntimeRunRegistry [ "read" ] > > ,
572+ serviceEvidence : RuntimeServiceEvidence [ ] ,
573+ preservePrimaryFailure : boolean ,
574+ cleanup : ( ) => Promise < void > ,
575+ ) : Promise < RunResourceCleanupEvidence > {
576+ try {
577+ return await runRecipeCleanup ( runRegistry , runRecord , cleanup )
578+ } catch ( error ) {
579+ if ( preservePrimaryFailure && error instanceof RunResourceCleanupError ) {
580+ return error . evidence
581+ }
582+ throw error
583+ } finally {
584+ await runRegistry . update ( runRecord . runId , {
585+ metadata : { managedRuntimeServices : serviceEvidence } ,
586+ } )
587+ }
588+ }
589+
541590async function recipeArtifactsMountConflictFailure ( options : RecipeRunOptions ) : Promise < RecipeRunOutput | undefined > {
542591 const recipePath = resolve ( options . recipePath )
543592 const recipeDirectory = dirname ( recipePath )
0 commit comments