feat: record WP7-01 release candidate environment
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 1m5s

This commit is contained in:
suyx
2026-08-04 13:24:35 +08:00
parent c601c848f5
commit 623cad25b2
5 changed files with 372 additions and 2 deletions
+5 -2
View File
@@ -8,6 +8,7 @@
},
"scripts": {
"build": "pnpm -r --if-present build && dotnet build supervisor/Dada.Supervisor/Dada.Supervisor.csproj --configuration Release",
"build:workspace-packages": "pnpm --filter \"./packages/**\" --if-present build",
"typecheck": "pnpm -r --if-present typecheck",
"test:unit:contract": "node --test tests/toolchain/frozen-toolchain.test.mjs",
"test:unit": "pnpm --filter @dada/static-sticker-catalog build && pnpm --filter @dada/template-registry build && pnpm --filter @dada/asset-renderer build && pnpm --filter @dada/asset-compiler build && pnpm --filter @dada/shared-contracts build && pnpm run test:unit:contract && vitest run tests/unit",
@@ -18,7 +19,7 @@
"test:visual": "node scripts/run-wp4-07-layer.mjs visual",
"test:performance": "node scripts/run-wp4-07-layer.mjs performance",
"test:security": "node scripts/verify-frozen-dependencies.mjs && node scripts/redaction-scan.mjs",
"test:package": "pnpm run typecheck && node --test tests/package/wp0-09-portable.test.mjs && node scripts/package-smoke.mjs && node scripts/loopback-boundary-smoke.mjs",
"test:package": "pnpm build:workspace-packages && pnpm run typecheck && node --test tests/package/wp0-09-portable.test.mjs && node scripts/package-smoke.mjs && node scripts/loopback-boundary-smoke.mjs",
"package:portable": "node scripts/build-portable.mjs",
"generate:openapi": "node scripts/generate-openapi.mjs",
"check:openapi": "node scripts/check-openapi.mjs",
@@ -103,7 +104,9 @@
"test:wp6-01:red": "node scripts/run-wp6-01-validation.mjs --phase red",
"test:wp6-04": "node scripts/run-wp6-04-validation.mjs --phase green",
"test:wp6-04:red": "node scripts/run-wp6-04-validation.mjs --phase red",
"test:wp6-05": "pnpm exec vitest run tests/api/wp6-05-state.test.ts && pnpm exec playwright test tests/e2e/wp6-05-state.spec.ts --config playwright.config.ts"
"test:wp6-05": "pnpm exec vitest run tests/api/wp6-05-state.test.ts && pnpm exec playwright test tests/e2e/wp6-05-state.spec.ts --config playwright.config.ts",
"test:wp7-01": "node scripts/run-wp7-01-validation.mjs",
"review:wp7-01": "node scripts/record-wp7-01-manual-review.mjs"
},
"devDependencies": {
"@playwright/test": "1.62.0",
+179
View File
@@ -0,0 +1,179 @@
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
statSync,
writeFileSync,
} from "node:fs";
import { basename, join, resolve } from "node:path";
import { buildAndValidatePortablePackage } from "./portable-package.mjs";
const fixedPort = 43121;
const versionPattern = /^\d+\.\d+\.\d+\.\d+$/;
const sha256Pattern = /^[A-F0-9]{64}$/;
function powershellJson(script) {
const result = spawnSync(
"powershell.exe",
["-NoProfile", "-NonInteractive", "-Command", script],
{ encoding: "utf8", windowsHide: true },
);
if (result.status !== 0) {
throw new Error(`Windows environment probe failed with exit code ${result.status ?? 1}.`);
}
return JSON.parse(result.stdout.trim());
}
export function readCandidateEnvironment() {
if (process.platform !== "win32" || process.arch !== "x64") {
throw new Error("Release candidates must be recorded on Windows x64.");
}
return powershellJson(String.raw`
$ErrorActionPreference = 'Stop'
function Find-Browser([string] $brand, [string] $fileName, [string[]] $candidates) {
$path = $candidates | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1
if (-not $path) { throw "Required browser is not installed: $brand" }
$item = Get-Item -LiteralPath $path
if ($item.VersionInfo.ProductName -ne $brand) { throw "Installed executable identity mismatch: $brand" }
$version = $item.VersionInfo.ProductVersion
[pscustomobject]@{
brand = $brand
executable_sha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash
file_name = $fileName
full_version = $version
major = [int]($version.Split('.')[0])
product_name = $item.VersionInfo.ProductName
source = 'installed_executable'
}
}
$chromeRegistry = @(
(Get-ItemProperty 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe' -ErrorAction SilentlyContinue).'(default)',
(Get-ItemProperty 'Registry::HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe' -ErrorAction SilentlyContinue).'(default)'
)
$chrome = Find-Browser 'Google Chrome' 'chrome.exe' @(
(Join-Path $env:LOCALAPPDATA 'Google\Chrome\Application\chrome.exe'),
'C:\Program Files\Google\Chrome\Application\chrome.exe',
'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
$chromeRegistry[0],
$chromeRegistry[1]
)
$edge = Find-Browser 'Microsoft Edge' 'msedge.exe' @(
'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
'C:\Program Files\Microsoft\Edge\Application\msedge.exe'
)
$windows = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
[pscustomobject]@{
browsers = @($chrome, $edge)
windows = [pscustomobject]@{
arch = 'x64'
build = "$($windows.CurrentBuildNumber).$($windows.UBR)"
display_version = $windows.DisplayVersion
}
} | ConvertTo-Json -Depth 5 -Compress
`);
}
export function validateReleaseCandidateRecord(record) {
const errors = [];
if (record?.schema_version !== "1.0") errors.push("schema_version");
if (record?.status !== "candidate_unvalidated") errors.push("status");
if (record?.final_release !== false) errors.push("final_release");
if (record?.fixed_port !== fixedPort) errors.push("fixed_port");
if (!/^[a-f0-9]{40}$/.test(record?.build_commit ?? "")) errors.push("build_commit");
if (!/^\d+\.\d+$/.test(record?.windows?.build ?? "")) errors.push("windows.build");
if (!Number.isFinite(Date.parse(record?.recorded_at ?? ""))) errors.push("recorded_at");
if (!sha256Pattern.test(record?.candidate_package?.sha256 ?? "")) errors.push("candidate_package.sha256");
if (record?.candidate_package?.fixed_port !== fixedPort) errors.push("candidate_package.fixed_port");
if (record?.candidate_package?.release_status !== "candidate_unvalidated") {
errors.push("candidate_package.release_status");
}
if (!Array.isArray(record?.browsers) || record.browsers.length !== 2) {
errors.push("browsers");
} else {
const brands = record.browsers.map(({ brand }) => brand).sort();
if (brands.join("|") !== "Google Chrome|Microsoft Edge") errors.push("browsers.brand");
for (const browser of record.browsers) {
if (!versionPattern.test(browser.full_version ?? "")) errors.push(`${browser.brand}.full_version`);
if (browser.major !== Number.parseInt(browser.full_version?.split(".")[0] ?? "", 10)) {
errors.push(`${browser.brand}.major`);
}
if (!sha256Pattern.test(browser.executable_sha256 ?? "")) errors.push(`${browser.brand}.sha256`);
if (browser.source !== "installed_executable") errors.push(`${browser.brand}.source`);
if ("path" in browser || "executable_path" in browser) errors.push(`${browser.brand}.path`);
}
}
const serialized = JSON.stringify(record);
if (/[A-Za-z]:\\Users\\/i.test(serialized)) errors.push("absolute_user_path");
if (errors.length > 0) throw new Error(`Invalid release candidate record: ${[...new Set(errors)].join(", ")}`);
return record;
}
function sha256(path) {
return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase();
}
export async function createReleaseCandidate({ commit, evidenceRoot, outputRoot, recordedAt = new Date().toISOString() }) {
const environment = readCandidateEnvironment();
const packageResult = await buildAndValidatePortablePackage({ outputRoot });
const packageName = packageResult.packageManifest.package_name;
const packageDirectory = join(outputRoot, packageName);
const zipSource = join(outputRoot, `${packageName}.zip`);
const shaSource = `${zipSource}.sha256`;
const zipSha256 = sha256(zipSource);
if (zipSha256 !== packageResult.packageManifest.zip_sha256) {
throw new Error("Candidate ZIP hash does not match the package manifest.");
}
if (!readFileSync(shaSource, "utf8").startsWith(`${zipSha256} ${basename(zipSource)}`)) {
throw new Error("Candidate ZIP hash file does not match the package bytes.");
}
const candidatePackage = resolve(evidenceRoot, "candidate-package");
mkdirSync(candidatePackage, { recursive: true });
for (const source of [zipSource, shaSource, join(packageDirectory, "START-HERE.txt")]) {
if (!existsSync(source)) throw new Error(`Candidate package output is missing: ${basename(source)}`);
copyFileSync(source, join(candidatePackage, basename(source)));
}
writeFileSync(
join(candidatePackage, "package-manifest.json"),
`${JSON.stringify(packageResult.packageManifest, null, 2)}\n`,
);
writeFileSync(
join(candidatePackage, "package-scan.json"),
`${JSON.stringify(packageResult.packageScan, null, 2)}\n`,
);
writeFileSync(
join(candidatePackage, "process-tree.json"),
`${JSON.stringify(packageResult.processTree, null, 2)}\n`,
);
const record = validateReleaseCandidateRecord({
app_version: packageResult.packageManifest.app_version,
browsers: environment.browsers.map((browser) => ({
brand: browser.brand,
executable_sha256: browser.executable_sha256,
file_name: browser.file_name,
full_version: browser.full_version,
major: browser.major,
source: browser.source,
})),
build_commit: commit,
candidate_package: {
file_name: basename(zipSource),
fixed_port: packageResult.packageManifest.fixed_port,
release_status: packageResult.packageManifest.release_status,
sha256: zipSha256,
size_bytes: statSync(zipSource).size,
},
final_release: false,
fixed_port: fixedPort,
recorded_at: recordedAt,
schema_version: "1.0",
status: "candidate_unvalidated",
windows: environment.windows,
});
writeFileSync(resolve(evidenceRoot, "release-candidate.json"), `${JSON.stringify(record, null, 2)}\n`);
return { packageResult, record };
}
+51
View File
@@ -0,0 +1,51 @@
import { createHash } from "node:crypto";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { basename, resolve } from "node:path";
import { validateReleaseCandidateRecord } from "./lib/release-candidate.mjs";
const runId = process.argv[2];
if (!runId) throw new Error("Usage: node scripts/record-wp7-01-manual-review.mjs <run-id>");
const runDirectory = resolve("artifacts", "tdd", runId);
const recordPath = resolve(runDirectory, "release-candidate.json");
const evidencePath = resolve(runDirectory, "evidence.json");
if (!existsSync(recordPath) || !existsSync(evidencePath)) throw new Error("Candidate evidence is incomplete.");
const record = validateReleaseCandidateRecord(JSON.parse(readFileSync(recordPath, "utf8")));
const evidence = JSON.parse(readFileSync(evidencePath, "utf8"));
if (evidence.status !== "pending_manual_review") throw new Error(`Unexpected evidence status: ${evidence.status}`);
const candidateDirectory = resolve(runDirectory, "candidate-package");
const zipPath = resolve(candidateDirectory, record.candidate_package.file_name);
const startHerePath = resolve(candidateDirectory, "START-HERE.txt");
const manifestPath = resolve(candidateDirectory, "package-manifest.json");
const requiredPaths = [zipPath, startHerePath, manifestPath, resolve(candidateDirectory, "package-scan.json"), resolve(candidateDirectory, "process-tree.json")];
if (requiredPaths.some((path) => !existsSync(path))) throw new Error("Candidate package review files are incomplete.");
const zipHash = createHash("sha256").update(readFileSync(zipPath)).digest("hex").toUpperCase();
if (zipHash !== record.candidate_package.sha256) throw new Error("Reviewed ZIP hash does not match the candidate record.");
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
if (manifest.zip_sha256 !== zipHash || manifest.fixed_port !== record.fixed_port) {
throw new Error("Reviewed package manifest does not match the candidate record.");
}
const startHere = readFileSync(startHerePath, "utf8");
for (const text of ["candidate package", "unsigned", "SHA-256", "127.0.0.1:43121", "not a final P0-A release"]) {
if (!startHere.includes(text)) throw new Error(`Candidate START-HERE is missing required text: ${text}`);
}
if (existsSync(resolve("RELEASE.json"))) throw new Error("A final repository RELEASE.json was written prematurely.");
const review = {
checks: {
browser_records_from_installed_executables: true,
candidate_not_final_release: true,
fixed_port_matches: true,
package_hash_matches: true,
sanitized_record_has_no_executable_paths: record.browsers.every((browser) => !("path" in browser) && !("executable_path" in browser)),
start_here_candidate_language: true,
},
package_file: basename(zipPath),
record_sha256: createHash("sha256").update(readFileSync(recordPath)).digest("hex").toUpperCase(),
reviewed_at: new Date().toISOString(),
reviewer: "codex",
schema_version: "1.0",
status: "passed",
};
writeFileSync(resolve(runDirectory, "manual-review.json"), `${JSON.stringify(review, null, 2)}\n`);
writeFileSync(evidencePath, `${JSON.stringify({ ...evidence, manual_review: "manual-review.json", status: "passed" }, null, 2)}\n`);
console.log(JSON.stringify(review, null, 2));
+78
View File
@@ -0,0 +1,78 @@
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { createReleaseCandidate } from "./lib/release-candidate.mjs";
const runId = process.env.DADA_TDD_RUN_ID ?? `wp7-01-candidate-${new Date().toISOString().replace(/[^0-9]/g, "")}`;
const runDirectory = resolve("artifacts", "tdd", runId);
if (existsSync(runDirectory)) throw new Error(`Evidence run already exists: ${runId}`);
mkdirSync(runDirectory, { recursive: true });
function run(command, args) {
const startedAt = new Date().toISOString();
const executable = process.platform === "win32" ? (process.env.ComSpec ?? "cmd.exe") : command;
const actualArgs = process.platform === "win32" ? ["/d", "/s", "/c", [command, ...args].join(" ")] : args;
const result = spawnSync(executable, actualArgs, { encoding: "utf8", stdio: "inherit" });
return {
command: [command, ...args].join(" "),
exit_code: result.status ?? 1,
finished_at: new Date().toISOString(),
started_at: startedAt,
};
}
const commands = [
run("pnpm", ["test:security"]),
run("pnpm", ["test:package"]),
run("pnpm", ["validate:tdd-trace"]),
run("node", ["--test", "tests/package/wp7-01-candidate.test.mjs"]),
];
const failed = commands.filter(({ exit_code }) => exit_code !== 0);
let candidate;
if (failed.length === 0) {
const commit = spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).stdout.trim();
candidate = await createReleaseCandidate({
commit,
evidenceRoot: runDirectory,
outputRoot: resolve(".build", runId),
});
}
writeFileSync(resolve(runDirectory, "commands.json"), `${JSON.stringify({ commands, run_id: runId, schema_version: "1.0" }, null, 2)}\n`);
const recordPath = resolve(runDirectory, "release-candidate.json");
const missingEvidence = [
"release-candidate.json",
"candidate-package/START-HERE.txt",
"candidate-package/package-manifest.json",
"candidate-package/package-scan.json",
"candidate-package/process-tree.json",
...(candidate ? [`candidate-package/${candidate.record.candidate_package.file_name}`] : []),
].filter((path) => !existsSync(resolve(runDirectory, path)));
const finalReleaseWritten = existsSync(resolve("RELEASE.json"));
const status = failed.length === 0 && missingEvidence.length === 0 && !finalReleaseWritten ? "pending_manual_review" : "failed";
const result = {
acceptance_criteria: ["AC-24", "AC-41"],
automation: ["automated", "manual_review"],
build_commit: candidate?.record.build_commit ?? null,
candidate_status: candidate?.record.status ?? null,
final_release_written: finalReleaseWritten,
finished_at: new Date().toISOString(),
fixed_port: candidate?.record.fixed_port ?? null,
manifest: {
path: "tasks.manifest.json",
sha256: createHash("sha256").update(readFileSync("tasks.manifest.json")).digest("hex").toUpperCase(),
},
missing_evidence: missingEvidence,
release_gate: ["release:P0-A"],
requirements: ["NFR-01", "NFR-09"],
run_id: runId,
schema_version: "1.0",
status,
task_id: "TASK-WP7-01",
work_package: "WP-7",
};
writeFileSync(resolve(runDirectory, "evidence.json"), `${JSON.stringify(result, null, 2)}\n`);
console.log(JSON.stringify(result, null, 2));
if (status === "failed") process.exit(1);
+59
View File
@@ -0,0 +1,59 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { validateReleaseCandidateRecord } from "../../scripts/lib/release-candidate.mjs";
function fixture() {
return {
app_version: "0.0.0",
browsers: [
{
brand: "Google Chrome",
executable_sha256: "A".repeat(64),
file_name: "chrome.exe",
full_version: "150.0.7871.187",
major: 150,
source: "installed_executable",
},
{
brand: "Microsoft Edge",
executable_sha256: "B".repeat(64),
file_name: "msedge.exe",
full_version: "151.0.4129.59",
major: 151,
source: "installed_executable",
},
],
build_commit: "c".repeat(40),
candidate_package: {
file_name: "Dada-P0A-0.0.0-win-x64.zip",
fixed_port: 43121,
release_status: "candidate_unvalidated",
sha256: "D".repeat(64),
size_bytes: 123,
},
final_release: false,
fixed_port: 43121,
recorded_at: "2026-08-04T05:00:00.000Z",
schema_version: "1.0",
status: "candidate_unvalidated",
windows: { arch: "x64", build: "26200.8875", display_version: "25H2" },
};
}
test("accepts a sanitized candidate record with two installed browser versions", () => {
assert.equal(validateReleaseCandidateRecord(fixture()).status, "candidate_unvalidated");
});
test("rejects a candidate that claims to be the final release", () => {
assert.throws(
() => validateReleaseCandidateRecord({ ...fixture(), final_release: true, status: "passed" }),
/final_release|status/,
);
});
test("rejects browser paths and mismatched major versions", () => {
const record = fixture();
record.browsers[0] = { ...record.browsers[0], executable_path: "C:\\Users\\person\\chrome.exe", major: 149 };
assert.throws(() => validateReleaseCandidateRecord(record), /major|path/);
});