|
1 | 1 | import { createHash } from "node:crypto" |
2 | | -import { readdir, readFile, stat, writeFile } from "node:fs/promises" |
| 2 | +import { cp, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises" |
| 3 | +import { tmpdir } from "node:os" |
3 | 4 | import { basename, dirname, join, resolve } from "node:path" |
4 | | -import { DEFAULT_WORDPRESS_VERSION, artifactBundleRunRef, artifactManifestFile, createBenchResultsJsonSchema, createRuntime, refreshArtifactManifestFileSha256s, stripUndefined, upsertArtifactManifestFiles, type ArtifactBundle, type ArtifactManifest, type ArtifactManifestFile, type BenchmarkArtifactRef, type BenchResults, type ExecutionResult, type Runtime, type RuntimeAssetSpec, type RuntimePreviewSpec, type RuntimeRunRecord, type RuntimeRunRegistry, type WorkspaceRecipe, type WorkspaceRecipeDeclaredArtifact, type WorkspaceRecipeFixtureDatabase, type WorkspaceRecipePluginRuntimeHealthProbe, type WorkspaceRecipeProbe, type WorkspaceRecipeSiteSeed } from "@automattic/wp-codebox-core" |
| 5 | +import { DEFAULT_WORDPRESS_VERSION, artifactBundleRunRef, artifactManifestFile, createBenchResultsJsonSchema, createRuntime, refreshArtifactManifestFileSha256s, stripUndefined, upsertArtifactManifestFiles, type ArtifactBundle, type ArtifactManifest, type ArtifactManifestFile, type BenchmarkArtifactRef, type BenchResults, type ExecutionResult, type Runtime, type RuntimeAssetSpec, type RuntimePreviewSpec, type RuntimeRunRecord, type RuntimeRunRegistry, type WorkspaceRecipe, type WorkspaceRecipeDeclaredArtifact, type WorkspaceRecipeFixtureDatabase, type WorkspaceRecipeMount, type WorkspaceRecipePluginRuntimeHealthProbe, type WorkspaceRecipeProbe, type WorkspaceRecipeSiteSeed } from "@automattic/wp-codebox-core" |
5 | 6 | import { createPlaygroundRuntimeBackend } from "@automattic/wp-codebox-playground" |
6 | 7 | import { Ajv2020 } from "ajv/dist/2020.js" |
7 | 8 | import { recipeExecutionSpec, sandboxWorkspaceContract } from "../agent-sandbox.js" |
@@ -125,6 +126,7 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru |
125 | 126 | let dependencyOverlays: PreparedDependencyOverlay[] = [] |
126 | 127 | let stagedFiles: PreparedStagedFile[] = [] |
127 | 128 | let overlays: PreparedRuntimeOverlay[] = [] |
| 129 | + const inputMountBaselinePaths: string[] = [] |
128 | 130 | let backendPackage: PreparedRuntimeBackendPackage | undefined |
129 | 131 | let runtime: Awaited<ReturnType<typeof createRuntime>> | undefined |
130 | 132 | const executions: RecipeExecutionResult[] = [] |
@@ -308,12 +310,13 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru |
308 | 310 |
|
309 | 311 | for (const mount of recipe.inputs?.mounts ?? []) { |
310 | 312 | const source = resolve(recipeDirectory, mount.source) |
| 313 | + const metadata = await inputMountMetadataWithBaseline(source, mount, inputMountBaselinePaths) |
311 | 314 | await awaitRecipe(`input.mount:${mount.target}`, runtime.mount({ |
312 | 315 | type: await recipeMountType(source, mount.type), |
313 | 316 | source, |
314 | 317 | target: mount.target, |
315 | 318 | mode: mount.mode ?? "readwrite", |
316 | | - metadata: mount.metadata, |
| 319 | + metadata, |
317 | 320 | })) |
318 | 321 | interruption?.throwIfInterrupted() |
319 | 322 | } |
@@ -416,6 +419,7 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru |
416 | 419 | cleanupEvidence = await runRecipeCleanup(runRegistry, runRecord, async () => { |
417 | 420 | await awaitRecipe("runtime.release", releaseRuntime(activeRuntime, successfulRecipe ? options.previewHoldSeconds : 0)) |
418 | 421 | await cleanupRecipePreparedSources(workspaceMounts, extraPlugins, stagedFiles, overlays, dependencyOverlays) |
| 422 | + await cleanupInputMountBaselines(inputMountBaselinePaths) |
419 | 423 | }) |
420 | 424 | runRecord = await runRegistry.read(runRecord.runId) |
421 | 425 | interruption?.throwIfInterrupted() |
@@ -529,7 +533,10 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru |
529 | 533 | } |
530 | 534 | } |
531 | 535 |
|
532 | | - cleanupEvidence = await runRecipeCleanup(runRegistry, runRecord, () => cleanupRecipePreparedSources(workspaceMounts, extraPlugins, stagedFiles, overlays, dependencyOverlays)) |
| 536 | + cleanupEvidence = await runRecipeCleanup(runRegistry, runRecord, async () => { |
| 537 | + await cleanupRecipePreparedSources(workspaceMounts, extraPlugins, stagedFiles, overlays, dependencyOverlays) |
| 538 | + await cleanupInputMountBaselines(inputMountBaselinePaths) |
| 539 | + }) |
533 | 540 | runRecord = await runRegistry.read(runRecord.runId) |
534 | 541 | const serializedError = interruption?.metadata ? recipeInterruptionSerializedError(interruption.metadata) : serializeRecipeRunError(error) |
535 | 542 | runRecord = await runRegistry.update(runRecord.runId, { |
@@ -1644,6 +1651,59 @@ function recipeRunMetadata(recipe: WorkspaceRecipe, recipePath: string, workspac |
1644 | 1651 | } |
1645 | 1652 | } |
1646 | 1653 |
|
| 1654 | +async function inputMountMetadataWithBaseline(source: string, mount: WorkspaceRecipeMount, cleanupPaths: string[]): Promise<Record<string, unknown> | undefined> { |
| 1655 | + const metadata = mount.metadata ? { ...mount.metadata } : {} |
| 1656 | + if ((mount.mode ?? "readwrite") !== "readwrite") { |
| 1657 | + return Object.keys(metadata).length > 0 ? metadata : undefined |
| 1658 | + } |
| 1659 | + if (typeof metadata.baselineSource === "string" && metadata.baselineSource.length > 0) { |
| 1660 | + return metadata |
| 1661 | + } |
| 1662 | + |
| 1663 | + let sourceStats |
| 1664 | + try { |
| 1665 | + sourceStats = await stat(source) |
| 1666 | + } catch { |
| 1667 | + return Object.keys(metadata).length > 0 ? metadata : undefined |
| 1668 | + } |
| 1669 | + if (!sourceStats.isDirectory() || await hasGitMetadata(source)) { |
| 1670 | + return Object.keys(metadata).length > 0 ? metadata : undefined |
| 1671 | + } |
| 1672 | + |
| 1673 | + const baselineSource = await mkdtemp(join(tmpdir(), "wp-codebox-input-mount-baseline-")) |
| 1674 | + cleanupPaths.push(baselineSource) |
| 1675 | + await cp(source, baselineSource, { |
| 1676 | + recursive: true, |
| 1677 | + filter: (entry) => shouldCopyInputMountBaselineEntry(source, entry), |
| 1678 | + }) |
| 1679 | + metadata.baselineSource = baselineSource |
| 1680 | + metadata.baselineStrategy = "input-mount-snapshot" |
| 1681 | + return metadata |
| 1682 | +} |
| 1683 | + |
| 1684 | +async function cleanupInputMountBaselines(paths: string[]): Promise<void> { |
| 1685 | + await Promise.all(paths.map((path) => rm(path, { recursive: true, force: true }))) |
| 1686 | + paths.length = 0 |
| 1687 | +} |
| 1688 | + |
| 1689 | +async function hasGitMetadata(directory: string): Promise<boolean> { |
| 1690 | + try { |
| 1691 | + await stat(join(directory, ".git")) |
| 1692 | + return true |
| 1693 | + } catch { |
| 1694 | + return false |
| 1695 | + } |
| 1696 | +} |
| 1697 | + |
| 1698 | +function shouldCopyInputMountBaselineEntry(sourceRoot: string, entry: string): boolean { |
| 1699 | + const relativePath = entry.slice(sourceRoot.length).replace(/^\/+/, "") |
| 1700 | + if (!relativePath) { |
| 1701 | + return true |
| 1702 | + } |
| 1703 | + const firstSegment = relativePath.split("/")[0] |
| 1704 | + return firstSegment !== ".git" && firstSegment !== "node_modules" |
| 1705 | +} |
| 1706 | + |
1647 | 1707 | function recipeRunDependencyOverlay(overlay: PreparedDependencyOverlay): Record<string, unknown> { |
1648 | 1708 | return { |
1649 | 1709 | source: overlay.source, |
|
0 commit comments