Skip to content

Commit 4bfbf2d

Browse files
authored
Merge pull request #2059 from Automattic/fix/2058-overlay-source-reference
Preserve portable dependency overlay source references
2 parents e4b4e63 + e28be8a commit 4bfbf2d

6 files changed

Lines changed: 68 additions & 7 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ export async function planWorkspaceRecipe(recipe: WorkspaceRecipe, recipeDirecto
376376
overlayKind: overlay.kind,
377377
package: overlay.package,
378378
source: overlay.source,
379+
...(overlay.reference ? { reference: overlay.reference } : {}),
379380
consumer: overlay.consumer,
380381
target,
381382
...(overlay.metadata ? { userMetadata: overlay.metadata } : {}),

packages/cli/src/recipe-sources.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ async function prepareRecipeDependencyOverlay(overlay: WorkspaceRecipeDependency
456456

457457
const source = resolve(recipeDirectory, overlay.source)
458458
await validateExistingDirectoryForOverlay(source, overlay.source)
459-
const reference = await resolvedGitSourceReference(source)
459+
const reference = overlay.reference ?? await resolvedGitSourceReference(source)
460460
const stagingRoot = await mkdtemp(join(tmpdir(), "wp-codebox-dependency-overlay-"))
461461
const hydratedSource = await prepareComposerBackedSource(source, stagingRoot, `dependency overlay ${overlay.package}`)
462462
// The overlay is mounted at the consumer's vendor path for the package and is

packages/cli/src/recipe-validation.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,9 @@ function validateRecipeDependencyOverlays(overlays: WorkspaceRecipeDependencyOve
497497
if (!overlay.source || typeof overlay.source !== "string") {
498498
throw new Error(`Recipe dependency overlays must include source: ${recipePath}`)
499499
}
500+
if (overlay.reference !== undefined && (typeof overlay.reference !== "string" || !/^[a-f0-9]{40,64}$/i.test(overlay.reference))) {
501+
throw new Error(`Recipe dependency overlay reference must be a 40-64 character hexadecimal immutable reference when provided: ${recipePath}`)
502+
}
500503
if (!overlay.consumer || typeof overlay.consumer !== "string") {
501504
throw new Error(`Recipe dependency overlays must include consumer: ${recipePath}`)
502505
}
@@ -681,6 +684,9 @@ export async function validateWorkspaceRecipeSemantics(recipe: WorkspaceRecipe,
681684
addIssue("invalid-composer-package", `${path}.package`, `Dependency overlay package must be a safe Composer package name: ${overlay.package}`)
682685
continue
683686
}
687+
if (overlay.reference !== undefined && (typeof overlay.reference !== "string" || !/^[a-f0-9]{40,64}$/i.test(overlay.reference))) {
688+
addIssue("invalid-dependency-overlay-reference", `${path}.reference`, "Dependency overlay reference must be a 40-64 character hexadecimal immutable reference when provided.")
689+
}
684690

685691
const consumerPlugin = recipeExtraPlugins(recipe).find((plugin) => recipeExtraPluginSlug(plugin) === overlay.consumer)
686692
const loadAs = consumerPlugin?.loadAs ?? "plugin"

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,11 @@ export function createWorkspaceRecipeJsonSchema(options: WorkspaceRecipeJsonSche
817817
kind: { const: "composer-package" },
818818
package: { type: "string", pattern: "^[a-z0-9_.-]+/[a-z0-9_.-]+$" },
819819
source: { type: "string" },
820+
reference: {
821+
type: "string",
822+
pattern: "^[a-fA-F0-9]{40,64}$",
823+
description: "Optional immutable source reference. When omitted, a clean local Git checkout may supply the reference.",
824+
},
820825
consumer: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9_-]*$" },
821826
metadata: { $ref: "#/$defs/metadata" },
822827
},

packages/runtime-core/src/runtime-contracts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ export interface WorkspaceRecipeDependencyOverlay {
516516
kind: "composer-package"
517517
package: string
518518
source: string
519+
reference?: string
519520
consumer: string
520521
metadata?: Record<string, unknown>
521522
}

scripts/composer-backed-source-hydration-smoke.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ import { tmpdir } from "node:os"
55
import { join } from "node:path"
66
import { promisify } from "node:util"
77
import { recipeRunDependencyOverlay } from "../packages/cli/src/commands/recipe-runtime-setup.js"
8+
import { validateWorkspaceRecipeSemantics } from "../packages/cli/src/recipe-validation.js"
89
import { prepareRecipeDependencyOverlays, prepareRecipeRuntimeOverlays } from "../packages/cli/src/recipe-sources.js"
910
import type { PreparedExtraPlugin } from "../packages/cli/src/recipe-sources.js"
11+
import { assertWorkspaceRecipeJsonSchema } from "../packages/runtime-core/src/recipe-schema.js"
1012
import type { WorkspaceRecipe } from "../packages/runtime-core/src/runtime-contracts.js"
1113

1214
const root = await mkdtemp(join(tmpdir(), "wp-codebox-runtime-overlay-hydration-"))
1315
const overlaySource = join(root, "php-ai-client")
1416
const dependencySource = join(root, "generic-composer-package")
1517
const nonGitDependencySource = join(root, "non-git-composer-package")
18+
const nonGitDependencyReference = "0123456789abcdef0123456789abcdef01234567"
1619
const binDir = join(root, "bin")
1720
const scoperPath = join(root, "php-scoper.phar")
1821
const originalPath = process.env.PATH
@@ -149,6 +152,49 @@ const recipe: WorkspaceRecipe = {
149152
},
150153
}
151154

155+
assertWorkspaceRecipeJsonSchema({
156+
schema: "wp-codebox/workspace-recipe/v1",
157+
workflow: { steps: [{ command: "wordpress.run-php", args: ["<?php"] }] },
158+
inputs: {
159+
dependency_overlays: [{
160+
kind: "composer-package",
161+
package: "acme/non-git-package",
162+
source: nonGitDependencySource,
163+
reference: nonGitDependencyReference,
164+
consumer: "consumer-plugin",
165+
}],
166+
},
167+
})
168+
assert.throws(() => assertWorkspaceRecipeJsonSchema({
169+
schema: "wp-codebox/workspace-recipe/v1",
170+
workflow: { steps: [{ command: "wordpress.run-php", args: ["<?php"] }] },
171+
inputs: {
172+
dependency_overlays: [{
173+
kind: "composer-package",
174+
package: "acme/non-git-package",
175+
source: nonGitDependencySource,
176+
reference: "not-an-immutable-reference",
177+
consumer: "consumer-plugin",
178+
}],
179+
},
180+
}))
181+
assert.deepEqual((await validateWorkspaceRecipeSemantics({
182+
workflow: { steps: [{ command: "wordpress.run-php", args: ["<?php"] }] },
183+
inputs: {
184+
dependency_overlays: [{
185+
kind: "composer-package",
186+
package: "acme/non-git-package",
187+
source: nonGitDependencySource,
188+
reference: "not-an-immutable-reference",
189+
consumer: "consumer-plugin",
190+
}],
191+
},
192+
}, root)).filter((issue) => issue.path === "$.inputs.dependency_overlays[0].reference"), [{
193+
code: "invalid-dependency-overlay-reference",
194+
path: "$.inputs.dependency_overlays[0].reference",
195+
message: "Dependency overlay reference must be a 40-64 character hexadecimal immutable reference when provided.",
196+
}])
197+
152198
const overlays = await prepareRecipeRuntimeOverlays(recipe, root)
153199
const consumers: PreparedExtraPlugin[] = [{
154200
source: consumerSource,
@@ -171,6 +217,7 @@ const dependencyOverlays = await prepareRecipeDependencyOverlays({
171217
kind: "composer-package",
172218
package: "acme/non-git-package",
173219
source: nonGitDependencySource,
220+
reference: nonGitDependencyReference,
174221
consumer: "consumer-plugin",
175222
}],
176223
},
@@ -188,25 +235,26 @@ try {
188235
assert.equal(dependencyOverlays[0].reference, dependencyReference.trim(), "clean Git source revision survives Composer staging")
189236
assert.equal(dependencyOverlays[0].metadata.reference, dependencyReference.trim(), "mounted dependency metadata preserves the source revision")
190237
assert.equal(recipeRunDependencyOverlay(dependencyOverlays[0]).reference, dependencyReference.trim(), "runtime dependency provenance exposes the source revision")
191-
assert.equal(dependencyOverlays[1].reference, undefined, "non-Git source has no fabricated revision")
192-
assert.equal(dependencyOverlays[1].metadata.reference, undefined, "non-Git source metadata omits the revision")
238+
assert.equal(dependencyOverlays[1].reference, nonGitDependencyReference, "declared reference supports non-Git sources")
239+
assert.equal(dependencyOverlays[1].metadata.reference, nonGitDependencyReference, "mounted dependency metadata preserves the declared reference")
240+
assert.equal(recipeRunDependencyOverlay(dependencyOverlays[1]).reference, nonGitDependencyReference, "runtime dependency provenance exposes the declared reference")
193241
assert.equal((JSON.parse(await readFile(join(consumerSource, "vendor", "composer", "installed.json"), "utf8")) as { packages: Array<{ source?: unknown }> }).packages[0].source, undefined, "original consumer Composer provenance remains unchanged")
194242
const runtimeInstalled = JSON.parse(await readFile(join(consumers[0].source, "vendor", "composer", "installed.json"), "utf8")) as { packages: Array<{ name: string, version: string, source?: { reference?: string } }> }
195243
assert.deepEqual(runtimeInstalled.packages[0], { name: "acme/package", version: "1.0.0+no-version-set", source: { reference: dependencyReference.trim() } }, "runtime Composer dependency provenance includes the immutable source reference")
196-
assert.deepEqual(runtimeInstalled.packages[1], { name: "acme/non-git-package", version: "1.0.0+no-version-set" }, "runtime Composer provenance omits unresolved source references")
244+
assert.deepEqual(runtimeInstalled.packages[1], { name: "acme/non-git-package", version: "1.0.0+no-version-set", source: { reference: nonGitDependencyReference } }, "runtime Composer provenance uses the declared source reference")
197245
const { stdout: runtimeInstalledPhp } = await execFile("php", ["-r", "echo json_encode(require $argv[1]);", join(consumers[0].source, "vendor", "composer", "installed.php")])
198246
const runtimePhpVersions = JSON.parse(runtimeInstalledPhp) as { versions: Record<string, { reference?: string | null }> }
199247
assert.equal(runtimePhpVersions.versions["acme/package"]?.reference, dependencyReference.trim(), "Composer runtime metadata includes the immutable source reference")
200-
assert.equal(runtimePhpVersions.versions["acme/non-git-package"]?.reference, null, "Composer runtime metadata leaves unresolved references unchanged")
248+
assert.equal(runtimePhpVersions.versions["acme/non-git-package"]?.reference, nonGitDependencyReference, "Composer runtime metadata includes the declared source reference")
201249
const runtimePackageRow = runtimePhpVersions.versions["acme/package"]
202250
assert.deepEqual(runtimePackageRow, {
203251
pretty_version: "1.0.0+no-version-set",
204252
reference: dependencyReference.trim(),
205253
}, "the final PHP-visible package row exposes the clean Git reference")
206254
assert.deepEqual(runtimePhpVersions.versions["acme/non-git-package"], {
207255
pretty_version: "1.0.0+no-version-set",
208-
reference: null,
209-
}, "the final PHP-visible package row leaves an unavailable reference unchanged")
256+
reference: nonGitDependencyReference,
257+
}, "the final PHP-visible package row exposes the declared reference")
210258
assert.match(await readFile(join(overlays[0].source, "src", "Client.php"), "utf8"), /WordPress\\AiClientDependencies\\Psr\\Log\\LoggerInterface/)
211259
} finally {
212260
await Promise.all([...overlays, ...dependencyOverlays, ...consumers].flatMap((overlay) => overlay.cleanupPaths).map((path) => rm(path, { recursive: true, force: true })))

0 commit comments

Comments
 (0)