Files
tyx_AI_xhs/scripts/lib/release-candidate.mjs
T
suyx 623cad25b2
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 1m5s
feat: record WP7-01 release candidate environment
2026-08-04 13:24:35 +08:00

180 lines
7.7 KiB
JavaScript

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 };
}