Skip to content

Commit b91c172

Browse files
authored
Fix recipe artifact evidence digest refresh
1 parent 270484e commit b91c172

2 files changed

Lines changed: 81 additions & 5 deletions

File tree

packages/cli/src/recipe-evidence.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"
44
import { dirname, join, relative, resolve } from "node:path"
55
import { fileURLToPath } from "node:url"
66
import { promisify } from "node:util"
7-
import { DEFAULT_CAPTURED_ARTIFACT_MAX_BYTES, DEFAULT_WORDPRESS_VERSION, STRUCTURED_ARTIFACT_INDEX_SCHEMA, artifactFileDigest, artifactManifestFileWithSha256, captureArtifactFile, checkWorkspacePolicy, materializeStructuredArtifactFiles, normalizeAgentTerminalResult, normalizeRuntimeBackendKind, normalizeStructuredArtifacts, refreshArtifactManifestFileSha256s, runtimeReferenceManifestDigest, runtimeReplayReferenceIndexDigest, upsertArtifactManifestFiles, type AgentTerminalResult, type ArtifactBundle, type ArtifactManifest, type ArtifactManifestFile, type ArtifactSpec, type ExecutionResult, type Runtime, type RuntimeInfo, type RuntimePolicy, type StructuredArtifactRef, type WorkspacePolicyResult, type WorkspaceRecipe } from "@automattic/wp-codebox-core"
7+
import { DEFAULT_CAPTURED_ARTIFACT_MAX_BYTES, DEFAULT_WORDPRESS_VERSION, STRUCTURED_ARTIFACT_INDEX_SCHEMA, artifactFileDigest, artifactManifestFileWithSha256, calculateArtifactManifestFileListDigest, captureArtifactFile, checkWorkspacePolicy, materializeStructuredArtifactFiles, normalizeAgentTerminalResult, normalizeRuntimeBackendKind, normalizeStructuredArtifacts, refreshArtifactManifestFileSha256s, runtimeReferenceManifestDigest, runtimeReplayReferenceIndexDigest, upsertArtifactManifestFiles, type AgentTerminalResult, type ArtifactBundle, type ArtifactManifest, type ArtifactManifestFile, type ArtifactSpec, type ExecutionResult, type Runtime, type RuntimeInfo, type RuntimePolicy, type StructuredArtifactRef, type WorkspacePolicyResult, type WorkspaceRecipe } from "@automattic/wp-codebox-core"
88
import { verifyArtifactBundle, type ArtifactBundleVerificationResult } from "@automattic/wp-codebox-core/artifacts"
99
import { isPlainObject as isRecord, sha256StableJson, stripUndefined } from "@automattic/wp-codebox-core/internals"
1010
import type { RecipeSecretEnvSummaryEntry } from "./recipe-secret-env.js"
@@ -1533,24 +1533,40 @@ async function writeRecipeEvidenceJson(artifactRoot: string, path: string, value
15331533
async function updateRecipeArtifactEvidenceReferences(artifacts: ArtifactBundle, evidenceFiles: RecipeArtifactEvidenceFile[]): Promise<void> {
15341534
const manifest = JSON.parse(await readFile(artifacts.manifestPath, "utf8")) as ArtifactManifest
15351535
upsertArtifactManifestFiles(manifest, evidenceFiles.map(evidenceFileToManifestFile))
1536+
refreshManifestFileListDigest(manifest, artifacts)
15361537

15371538
const evidence = Object.fromEntries(evidenceFiles.map((file) => [file.kind, { path: file.path, sha256: file.sha256 }]))
15381539
const metadata = JSON.parse(await readFile(artifacts.metadataPath, "utf8")) as Record<string, unknown>
15391540
const metadataArtifacts = isRecord(metadata.artifacts) ? metadata.artifacts : {}
15401541
const metadataEvidence = isRecord(metadata.evidence) ? metadata.evidence : {}
1542+
metadata.id = manifest.id
1543+
metadata.contentDigest = manifest.contentDigest
15411544
metadata.artifacts = { ...metadataArtifacts, runtimeEvidence: { ...(isRecord(metadataArtifacts.runtimeEvidence) ? metadataArtifacts.runtimeEvidence : {}), ...evidence } }
15421545
metadata.evidence = { ...metadataEvidence, runtimeEvidence: { ...(isRecord(metadataEvidence.runtimeEvidence) ? metadataEvidence.runtimeEvidence : {}), ...evidence } }
15431546
await writeFile(artifacts.metadataPath, `${JSON.stringify(metadata, null, 2)}\n`)
15441547

15451548
const review = JSON.parse(await readFile(artifacts.reviewPath, "utf8")) as Record<string, unknown>
15461549
const reviewEvidence = isRecord(review.evidence) ? review.evidence : {}
1547-
review.evidence = { ...reviewEvidence, runtimeEvidence: { ...(isRecord(reviewEvidence.runtimeEvidence) ? reviewEvidence.runtimeEvidence : {}), ...evidence } }
1550+
review.artifactId = manifest.id
1551+
review.evidence = { ...reviewEvidence, artifactContentDigest: manifest.contentDigest.value, runtimeEvidence: { ...(isRecord(reviewEvidence.runtimeEvidence) ? reviewEvidence.runtimeEvidence : {}), ...evidence } }
15481552
await writeFile(artifacts.reviewPath, `${JSON.stringify(review, null, 2)}\n`)
15491553

15501554
await refreshManifestAfterEvidenceMutation(artifacts.directory, manifest)
15511555
await writeFile(artifacts.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
15521556
}
15531557

1558+
function refreshManifestFileListDigest(manifest: ArtifactManifest, artifacts: ArtifactBundle): void {
1559+
if (manifest.contentDigest.inputs.length !== 1 || manifest.contentDigest.inputs[0] !== "manifest.files") {
1560+
return
1561+
}
1562+
1563+
const value = calculateArtifactManifestFileListDigest(manifest.files)
1564+
manifest.contentDigest = { algorithm: "sha256", inputs: ["manifest.files"], value }
1565+
manifest.id = `artifact-bundle-sha256-${value}`
1566+
artifacts.id = manifest.id
1567+
artifacts.contentDigest = value
1568+
}
1569+
15541570
async function refreshManifestAfterEvidenceMutation(artifactRoot: string, manifest: ArtifactManifest): Promise<void> {
15551571
// Runtime reference files embed manifest hashes, then become manifest-hashed artifacts themselves.
15561572
await refreshArtifactManifestFileSha256s(artifactRoot, manifest)

scripts/artifact-bundle-verifier-smoke.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import assert from "node:assert/strict"
22
import { execFile } from "node:child_process"
33
import { createHash } from "node:crypto"
4-
import { cp, link, mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"
4+
import { cp, link, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"
55
import { tmpdir } from "node:os"
66
import { join, resolve } from "node:path"
77
import { promisify } from "node:util"
8-
import { calculateArtifactContentDigest, calculateArtifactManifestFileSha256 } from "@automattic/wp-codebox-core"
8+
import { calculateArtifactContentDigest, calculateArtifactManifestFileListDigest, calculateArtifactManifestFileSha256 } from "@automattic/wp-codebox-core"
99
import { preflightArtifactBundleApply, verifyArtifactBundle } from "@automattic/wp-codebox-core/artifacts"
10-
import type { ArtifactManifest } from "@automattic/wp-codebox-core"
10+
import type { ArtifactBundle, ArtifactManifest } from "@automattic/wp-codebox-core"
11+
import { appendRecipeRuntimeEvidence } from "../packages/cli/src/recipe-evidence.js"
1112

1213
const execFileAsync = promisify(execFile)
1314
const root = resolve(import.meta.dirname, "..")
@@ -36,6 +37,15 @@ try {
3637
const artifactsOption = await execFileAsync(process.execPath, ["packages/cli/dist/index.js", "artifacts", "verify", "--artifacts", validBundle, "--json"], { cwd: root })
3738
assert.equal(JSON.parse(artifactsOption.stdout).valid, true)
3839

40+
const recipeEvidenceBundle = await copyBundle(validBundle, join(workspace, "recipe-evidence-appended"))
41+
await rewriteBundleToManifestFileListDigest(recipeEvidenceBundle)
42+
const recipeArtifacts = await artifactBundleFixture(recipeEvidenceBundle)
43+
await appendRecipeRuntimeEvidence(recipeArtifacts, [{ filename: "run-attestation.json", kind: "run-attestation", value: { schema: "fixture/run-attestation/v1", status: "passed" } }])
44+
const recipeEvidenceVerification = await verifyArtifactBundle(recipeEvidenceBundle)
45+
assert.equal(recipeEvidenceVerification.valid, true)
46+
assert.equal(recipeEvidenceVerification.manifest?.contentDigest.value, recipeArtifacts.contentDigest)
47+
assert.equal(recipeEvidenceVerification.manifest?.files.some((file) => file.path === "files/runtime-evidence/run-attestation.json"), true)
48+
3949
const missingManifest = join(workspace, "missing-manifest")
4050
await mkdir(missingManifest, { recursive: true })
4151
assertViolation(await verifyArtifactBundle(missingManifest), "missing-manifest")
@@ -161,6 +171,56 @@ async function writeValidBundle(directory: string): Promise<void> {
161171
await writeJson(join(directory, "manifest.json"), manifest)
162172
}
163173

174+
async function rewriteBundleToManifestFileListDigest(directory: string): Promise<void> {
175+
const manifest = JSON.parse(await readFile(join(directory, "manifest.json"), "utf8")) as ArtifactManifest
176+
const digest = calculateArtifactManifestFileListDigest(manifest.files)
177+
manifest.id = `artifact-bundle-sha256-${digest}`
178+
manifest.contentDigest = { algorithm: "sha256", inputs: ["manifest.files"], value: digest }
179+
await writeJson(join(directory, "files/review.json"), reviewFixture(digest))
180+
await writeJson(join(directory, "metadata.json"), {
181+
id: manifest.id,
182+
contentDigest: manifest.contentDigest,
183+
artifacts: {
184+
changedFiles: "files/changed-files.json",
185+
patch: "files/patch.diff",
186+
review: "files/review.json",
187+
testResults: "files/test-results.json",
188+
runtimeEpisodeTrace: "files/runtime-episode-trace.json",
189+
runtimeEpisodeEvents: "files/runtime-episode.jsonl",
190+
},
191+
})
192+
await attachManifestFileHashes(directory, manifest)
193+
await writeJson(join(directory, "manifest.json"), manifest)
194+
}
195+
196+
async function artifactBundleFixture(directory: string): Promise<ArtifactBundle> {
197+
const manifest = JSON.parse(await readFile(join(directory, "manifest.json"), "utf8")) as ArtifactManifest
198+
return {
199+
id: manifest.id,
200+
directory,
201+
manifestPath: join(directory, "manifest.json"),
202+
metadataPath: join(directory, "metadata.json"),
203+
blueprintAfterPath: join(directory, "blueprint.after.json"),
204+
blueprintAfterNotesPath: join(directory, "blueprint.after-notes.json"),
205+
eventsPath: join(directory, "events.jsonl"),
206+
commandsPath: join(directory, "commands.jsonl"),
207+
observationsPath: join(directory, "observations.jsonl"),
208+
runtimeLogPath: join(directory, "logs/runtime.log"),
209+
commandsLogPath: join(directory, "logs/commands.log"),
210+
mountsPath: join(directory, "files/mounts.json"),
211+
capturedMountsPath: join(directory, "files/mounted-files.json"),
212+
diffsPath: join(directory, "files/diffs.json"),
213+
workspacePatchPath: join(directory, "files/workspace-patch.json"),
214+
changedFilesPath: join(directory, "files/changed-files.json"),
215+
patchPath: join(directory, "files/patch.diff"),
216+
diagnosticsPath: join(directory, "files/diagnostics.json"),
217+
testResultsPath: join(directory, "files/test-results.json"),
218+
reviewPath: join(directory, "files/review.json"),
219+
contentDigest: manifest.contentDigest.value,
220+
createdAt: manifest.createdAt,
221+
}
222+
}
223+
164224
function manifestFixture(digest: string): ArtifactManifest {
165225
return {
166226
id: `artifact-bundle-sha256-${digest}`,

0 commit comments

Comments
 (0)