feat: implement TASK-WP0-09 packaging
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 42s

This commit is contained in:
suyx
2026-07-28 00:55:18 +08:00
parent 361bda2f22
commit 2c5c1a5005
13 changed files with 600 additions and 8 deletions
+41
View File
@@ -0,0 +1,41 @@
name: Dada P0-A isolated Windows CI
on:
push:
pull_request:
workflow_dispatch:
jobs:
validate-and-package:
runs-on: [self-hosted, windows, x64, dada-isolated]
timeout-minutes: 60
env:
CI: "true"
DADA_EXTERNAL_MODE: mock
steps:
- name: Check out frozen revision
uses: actions/checkout@v4
- name: Enable frozen package manager
shell: powershell
run: corepack enable
- name: Install frozen dependencies
shell: powershell
run: pnpm install --frozen-lockfile
- name: Verify isolated CI policy
shell: powershell
run: node scripts/verify-ci-isolation.mjs
- name: Run complete automated suite
shell: powershell
run: pnpm test:all
- name: Build candidate portable package
shell: powershell
run: pnpm package:portable
- name: Upload candidate package and evidence
uses: actions/upload-artifact@v4
with:
name: dada-p0a-candidate-${{ gitea.sha }}
path: |
.build/portable-release/*.zip
.build/portable-release/*.sha256
artifacts/tdd/**
if-no-files-found: error
+1 -1
View File
@@ -59,7 +59,7 @@ export interface CreateAppOptions {
publicAssets?: PublicAssetResolver;
}
const supportGateDirectory = resolve("apps/web/support-gate");
const supportGateDirectory = resolve(process.env.DADA_SUPPORT_GATE_ROOT ?? "apps/web/support-gate");
const supportGateHtml = readFileSync(resolve(supportGateDirectory, "index.html"), "utf8");
const supportGateCss = readFileSync(resolve(supportGateDirectory, "support-gate.css"), "utf8");
const supportGateJavaScript = readFileSync(resolve(supportGateDirectory, "support-gate.js"), "utf8");
+2 -1
View File
@@ -192,7 +192,8 @@ export function resolvePathWithinRoot(root: string, objectKey: string) {
}
function openInstanceDatabase(databasePath: string) {
const database = new Database(databasePath);
const nativeBinding = process.env.DADA_SQLITE_NATIVE_BINDING;
const database = new Database(databasePath, nativeBinding ? { nativeBinding } : undefined);
database.pragma("journal_mode = WAL");
database.exec(`
CREATE TABLE instance_metadata (
+2 -1
View File
@@ -146,7 +146,8 @@ export class ManagedStorage {
throw new Error("database_outside_data_root");
}
mkdirSync(dirname(this.databasePath), { recursive: true });
this.database = new Database(this.databasePath);
const nativeBinding = process.env.DADA_SQLITE_NATIVE_BINDING;
this.database = new Database(this.databasePath, nativeBinding ? { nativeBinding } : undefined);
this.database.pragma("journal_mode = WAL");
this.database.pragma("foreign_keys = ON");
this.database.pragma("busy_timeout = 5000");
+2 -1
View File
@@ -9,7 +9,8 @@ export class WorkerStorageStatus {
private readonly database: BetterSqlite3.Database;
constructor(databasePath: string) {
this.database = new Database(databasePath);
const nativeBinding = process.env.DADA_SQLITE_NATIVE_BINDING;
this.database = new Database(databasePath, nativeBinding ? { nativeBinding } : undefined);
this.database.pragma("busy_timeout = 5000");
}
+12
View File
@@ -0,0 +1,12 @@
{
"schema_version": "1.0",
"required_labels": ["self-hosted", "windows", "x64", "dada-isolated"],
"capacity": 1,
"external_mode": "mock",
"forbidden_inputs": [
"windows_credential_manager",
"LocalDataRoot",
"canonical_asset_archive"
],
"artifact_scope": ["portable-package", "tdd-evidence"]
}
+5 -2
View File
@@ -18,7 +18,8 @@
"test:visual": "node scripts/validate-layer-scope.mjs VISUAL",
"test:performance": "node scripts/validate-layer-scope.mjs PERFORMANCE",
"test:security": "node scripts/verify-frozen-dependencies.mjs && node scripts/redaction-scan.mjs",
"test:package": "pnpm run typecheck && pnpm --filter @dada/shared-contracts build && pnpm --filter @dada/web build && pnpm --filter @dada/api build && pnpm --filter @dada/worker build && node scripts/package-smoke.mjs && node scripts/loopback-boundary-smoke.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",
"package:portable": "node scripts/build-portable.mjs",
"generate:openapi": "node scripts/generate-openapi.mjs",
"check:openapi": "node scripts/check-openapi.mjs",
"validate:tdd-trace": "node scripts/validate-tdd-trace.mjs",
@@ -36,7 +37,9 @@
"test:wp0-07": "node scripts/run-wp0-07-validation.mjs",
"test:wp0-07:red": "node scripts/run-wp0-07-validation.mjs --phase red",
"test:wp0-08": "node scripts/run-wp0-08-validation.mjs",
"test:wp0-08:red": "node scripts/run-wp0-08-validation.mjs --phase red"
"test:wp0-08:red": "node scripts/run-wp0-08-validation.mjs --phase red",
"test:wp0-09": "node scripts/run-wp0-09-validation.mjs",
"test:wp0-09:red": "node scripts/run-wp0-09-validation.mjs --phase red"
},
"devDependencies": {
"@playwright/test": "1.62.0",
+12
View File
@@ -0,0 +1,12 @@
import { resolve } from "node:path";
import { buildAndValidatePortablePackage } from "./lib/portable-package.mjs";
const outputIndex = process.argv.indexOf("--output");
const outputRoot = outputIndex >= 0 ? resolve(process.argv[outputIndex + 1]) : resolve(".build", "portable-release");
const result = await buildAndValidatePortablePackage({ outputRoot });
console.log(JSON.stringify({
package: result.packageManifest.package_name,
sha256: result.packageManifest.zip_sha256,
status: result.status,
}, null, 2));
+395
View File
@@ -0,0 +1,395 @@
import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import { spawn, spawnSync } from "node:child_process";
import {
copyFileSync,
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmdirSync,
statSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join, relative, resolve, sep } from "node:path";
import { frozenRuntime } from "../frozen-versions.mjs";
const repositoryRoot = resolve(import.meta.dirname, "..", "..");
const fixedPort = 43121;
const textExtensions = new Set([".cjs", ".css", ".html", ".js", ".json", ".mjs", ".txt", ".xml"]);
const developmentOnlyDependencyDirectories = new Set([".github", "benchmark", "benchmarks", "docs", "examples", "test", "tests"]);
function debug(message) {
if (process.env.DADA_PACKAGE_DEBUG === "1") process.stderr.write(`[portable-package] ${message}\n`);
}
function run(command, args, options = {}) {
const executable = process.platform === "win32" && command === "pnpm" ? (process.env.ComSpec ?? "cmd.exe") : command;
const actualArgs = executable === command ? args : ["/d", "/s", "/c", [command, ...args].join(" ")];
const result = spawnSync(executable, actualArgs, { cwd: repositoryRoot, encoding: "utf8", stdio: "pipe", ...options });
if (result.status !== 0) {
throw new Error(`${command} ${args.join(" ")} failed:\n${result.stdout ?? ""}\n${result.stderr ?? ""}`);
}
return result.stdout?.trim() ?? "";
}
function ensureBuildOutput(path) {
const buildRoot = resolve(repositoryRoot, ".build");
const resolved = resolve(path);
if (resolved !== buildRoot && !resolved.startsWith(`${buildRoot}${sep}`)) {
throw new Error("Portable package output must remain under the repository .build directory.");
}
return resolved;
}
function json(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function writeJson(path, value) {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
}
function copyTree(source, destination, filter = () => true) {
if (!filter(source)) return;
const attributes = statSync(source);
if (attributes.isDirectory()) {
mkdirSync(destination, { recursive: true });
for (const entry of readdirSync(source)) {
copyTree(join(source, entry), join(destination, entry), filter);
}
return;
}
if (attributes.isFile()) copyFileSync(source, destination);
}
function removeTree(path) {
if (!existsSync(path)) return;
const attributes = lstatSync(path);
if (!attributes.isDirectory() || attributes.isSymbolicLink()) {
unlinkSync(path);
return;
}
for (const entry of readdirSync(path)) removeTree(join(path, entry));
rmdirSync(path);
}
function packageRootFromEntry(entryPath, expectedName) {
let current = dirname(entryPath);
while (current !== dirname(current)) {
const manifestPath = join(current, "package.json");
if (existsSync(manifestPath) && json(manifestPath).name === expectedName) return current;
current = dirname(current);
}
throw new Error(`Cannot resolve package root for ${expectedName}.`);
}
function copyRuntimeDependencies(sourceRoot, destinationRoot, rootNames) {
debug(`resolve dependencies for ${relative(repositoryRoot, sourceRoot)}`);
const records = new Map();
function copyResolved(name, requireFrom, destinationNodeModules, ancestors) {
let entry;
try {
entry = requireFrom.resolve(name);
} catch (error) {
throw new Error(`Runtime dependency ${name} is unavailable from ${sourceRoot}.`, { cause: error });
}
const root = packageRootFromEntry(entry, name);
const manifest = json(join(root, "package.json"));
const identity = `${manifest.name}@${manifest.version}`;
const destination = join(destinationNodeModules, ...name.split("/"));
debug(`copy dependency ${identity}`);
mkdirSync(dirname(destination), { recursive: true });
copyTree(root, destination, (source) => {
const name = basename(source);
return name !== "node_modules" && !developmentOnlyDependencyDirectories.has(name);
});
records.set(identity, { name: manifest.name, version: manifest.version });
if (ancestors.has(identity)) return;
const nestedAncestors = new Set(ancestors).add(identity);
const nestedRequire = createRequire(join(root, "package.json"));
for (const dependency of Object.keys(manifest.dependencies ?? {})) {
copyResolved(dependency, nestedRequire, join(destination, "node_modules"), nestedAncestors);
}
}
const rootRequire = createRequire(join(sourceRoot, "package.json"));
for (const name of rootNames) copyResolved(name, rootRequire, join(destinationRoot, "node_modules"), new Set());
return [...records.values()]
.sort((left, right) => left.name.localeCompare(right.name));
}
function listFiles(root) {
const files = [];
const reparsePoints = [];
function walk(current) {
for (const entry of readdirSync(current, { withFileTypes: true })) {
const path = join(current, entry.name);
const attributes = lstatSync(path);
if (attributes.isSymbolicLink()) reparsePoints.push(relative(root, path).replaceAll("\\", "/"));
else if (entry.isDirectory()) walk(path);
else if (entry.isFile()) files.push(path);
}
}
walk(root);
return { files, reparsePoints };
}
function fileSha256(path) {
return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase();
}
function copyApplication(source, destination, runtimeDependencies) {
mkdirSync(destination, { recursive: true });
copyTree(join(source, "dist"), join(destination, "dist"));
writeJson(join(destination, "package.json"), { private: true, type: "module" });
return copyRuntimeDependencies(source, destination, runtimeDependencies);
}
function buildArtifacts(stagingRoot) {
debug("build workspace artifacts");
run("pnpm", ["--filter", "@dada/shared-contracts", "build"]);
run("pnpm", ["--filter", "@dada/web", "build"]);
run("pnpm", ["--filter", "@dada/api", "build"]);
run("pnpm", ["--filter", "@dada/worker", "build"]);
run("dotnet", ["restore", "supervisor/Dada.Supervisor/Dada.Supervisor.csproj", "--configfile", "NuGet.Config"]);
run("dotnet", [
"publish",
"supervisor/Dada.Supervisor/Dada.Supervisor.csproj",
"--configuration", "Release",
"--no-restore",
"--output", stagingRoot,
"-p:DebugType=None",
"-p:DebugSymbols=false",
]);
}
function createZip(packageDirectory, zipPath) {
const escapedPackage = packageDirectory.replaceAll("'", "''");
const escapedZip = zipPath.replaceAll("'", "''");
run("powershell.exe", [
"-NoProfile",
"-NonInteractive",
"-Command",
`Compress-Archive -LiteralPath '${escapedPackage}' -DestinationPath '${escapedZip}' -CompressionLevel Optimal`,
]);
}
async function waitForHealth(child) {
const deadline = Date.now() + 15_000;
let lastError;
while (Date.now() < deadline) {
if (child.exitCode !== null) throw new Error(`Packaged API exited early with code ${child.exitCode}.`);
try {
const response = await fetch(`http://127.0.0.1:${fixedPort}/healthz`, { headers: { Host: `127.0.0.1:${fixedPort}` } });
if (response.ok) return response.json();
} catch (error) {
lastError = error;
}
await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
}
throw new Error("Packaged API did not become healthy on fixed port 43121.", { cause: lastError });
}
async function verifyExtractedPackage(zipPath, packageName) {
const extractRoot = mkdtempSync(join(tmpdir(), "dada-wp0-09-"));
try {
const escapedZip = zipPath.replaceAll("'", "''");
const escapedExtract = extractRoot.replaceAll("'", "''");
run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `Expand-Archive -LiteralPath '${escapedZip}' -DestinationPath '${escapedExtract}'`]);
const packageRoot = join(extractRoot, packageName);
const nativeResult = spawnSync(join(packageRoot, "runtime", "node.exe"), [join(packageRoot, "server", "native-smoke.cjs")], {
cwd: packageRoot,
encoding: "utf8",
});
if (nativeResult.status !== 0) throw new Error(`Packaged native module failed:\n${nativeResult.stderr ?? ""}`);
const native = JSON.parse(nativeResult.stdout.trim());
const supervisorProbe = spawnSync(join(packageRoot, "Dada.exe"), ["--package-layout-probe"], {
cwd: packageRoot,
encoding: "utf8",
windowsHide: true,
});
if (supervisorProbe.status !== 2) throw new Error(`Packaged Dada.exe usage probe exited with ${supervisorProbe.status}.`);
const api = spawn(join(packageRoot, "runtime", "node.exe"), [join(packageRoot, "server", "api.mjs")], {
cwd: packageRoot,
env: { ...process.env, DADA_SUPPORT_GATE_ROOT: join(packageRoot, "web", "support-gate") },
stdio: "ignore",
windowsHide: true,
});
try {
const health = await waitForHealth(api);
const releaseGate = await fetch(`http://127.0.0.1:${fixedPort}/api/v1/support/check`, {
body: JSON.stringify({
brands: [{ brand: "Google Chrome", version: "150" }],
full_version_list: [{ brand: "Google Chrome", version: "150.0.0.0" }],
platform: "Windows",
}),
headers: {
"content-type": "application/json",
"sec-ch-ua": '"Google Chrome";v="150"',
"sec-ch-ua-full-version-list": '"Google Chrome";v="150.0.0.0"',
"sec-ch-ua-platform": '"Windows"',
},
method: "POST",
});
if (releaseGate.status !== 426) throw new Error(`Candidate RELEASE.json unexpectedly passed with ${releaseGate.status}.`);
return {
api: { executable: "runtime/node.exe", health, pid: api.pid, release_gate: { status_code: releaseGate.status }, status: "passed" },
native,
supervisor: { credential_store_access: false, executable: "Dada.exe", exit_code: supervisorProbe.status, status: "passed" },
};
} finally {
if (api.exitCode === null) {
const exited = new Promise((resolveExit) => api.once("exit", resolveExit));
api.kill();
await exited;
}
}
} finally {
removeTree(extractRoot);
}
}
function scanPackage(packageDirectory) {
const { files, reparsePoints } = listFiles(packageDirectory);
const disallowedMatches = [];
const repositoryPath = repositoryRoot.toLowerCase();
const windowsUserPath = `${process.env.SystemDrive ?? "C:"}\\Users\\`.toLowerCase();
const userName = (process.env.USERNAME ?? "").toLowerCase();
for (const path of files) {
const extension = path.slice(path.lastIndexOf(".")).toLowerCase();
if (!textExtensions.has(extension)) continue;
const content = readFileSync(path, "utf8").toLowerCase();
const relativePath = relative(packageDirectory, path).replaceAll("\\", "/");
if (content.includes(repositoryPath)) disallowedMatches.push({ path: relativePath, rule: "repository_absolute_path" });
if (content.includes(windowsUserPath)) disallowedMatches.push({ path: relativePath, rule: "windows_user_path" });
if (userName.length >= 3 && content.includes(userName)) disallowedMatches.push({ path: relativePath, rule: "windows_username" });
if (/\b(?:sk|key)-[a-z0-9_-]{16,}\b/i.test(content)) disallowedMatches.push({ path: relativePath, rule: "credential_shape" });
if (!relativePath.includes("/node_modules/") && !relativePath.startsWith("LICENSES/")) {
for (const email of content.match(/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi) ?? []) {
if (!email.endsWith(".invalid")) disallowedMatches.push({ path: relativePath, rule: "non_fixture_email" });
}
}
}
return { disallowed_matches: disallowedMatches, reparse_points: reparsePoints, scanned_files: files.length, status: disallowedMatches.length === 0 && reparsePoints.length === 0 ? "passed" : "failed" };
}
export async function buildAndValidatePortablePackage({ evidenceDirectory, outputRoot }) {
if (process.platform !== frozenRuntime.os || process.arch !== frozenRuntime.arch || process.version.slice(1) !== frozenRuntime.node) {
throw new Error("Portable package build requires frozen Node 24.13.0 on win-x64.");
}
const resolvedOutput = ensureBuildOutput(outputRoot);
removeTree(resolvedOutput);
mkdirSync(resolvedOutput, { recursive: true });
const packageManifest = json(join(repositoryRoot, "package.json"));
const appVersion = packageManifest.version;
const packageName = `Dada-P0A-${appVersion}-win-x64`;
const packageDirectory = join(resolvedOutput, packageName);
const supervisorPublish = join(resolvedOutput, "supervisor-publish");
buildArtifacts(supervisorPublish);
debug("assemble package layout");
mkdirSync(packageDirectory, { recursive: true });
const supervisorExecutable = join(supervisorPublish, "Dada.Supervisor.exe");
if (!existsSync(supervisorExecutable)) throw new Error("Supervisor publish did not produce Dada.Supervisor.exe.");
copyFileSync(supervisorExecutable, join(packageDirectory, "Dada.exe"));
for (const name of ["Dada.Supervisor.deps.json", "Dada.Supervisor.dll", "Dada.Supervisor.runtimeconfig.json"]) {
copyFileSync(join(supervisorPublish, name), join(packageDirectory, name));
}
mkdirSync(join(packageDirectory, "runtime"), { recursive: true });
copyFileSync(process.execPath, join(packageDirectory, "runtime", "node.exe"));
const serverRoot = join(packageDirectory, "server");
debug("copy API application");
const apiDependencies = copyApplication(join(repositoryRoot, "apps", "api"), join(serverRoot, "api"), ["@fastify/swagger", "@sinclair/typebox", "better-sqlite3", "fastify"]);
debug("copy Worker application");
const workerDependencies = copyApplication(join(repositoryRoot, "apps", "worker"), join(serverRoot, "worker"), ["better-sqlite3"]);
const sharedDestination = join(serverRoot, "api", "node_modules", "@dada", "shared-contracts");
mkdirSync(sharedDestination, { recursive: true });
copyTree(join(repositoryRoot, "packages", "shared-contracts", "dist"), join(sharedDestination, "dist"));
copyFileSync(join(repositoryRoot, "packages", "shared-contracts", "package.json"), join(sharedDestination, "package.json"));
writeFileSync(join(serverRoot, "api.mjs"), 'import "./api/dist/main.js";\n');
writeFileSync(join(serverRoot, "worker.mjs"), 'import "./worker/dist/worker.js";\n');
const nativeSource = join(repositoryRoot, "apps", "api", "node_modules", "better-sqlite3", "prebuilds", "win32-x64.node");
mkdirSync(join(serverRoot, "native"), { recursive: true });
copyFileSync(nativeSource, join(serverRoot, "native", "better_sqlite3.node"));
writeFileSync(join(serverRoot, "native-smoke.cjs"), [
'"use strict";',
'const { createRequire } = require("node:module");',
'const { join } = require("node:path");',
'const requireApi = createRequire(join(__dirname, "api", "package.json"));',
'const Database = requireApi("better-sqlite3");',
'const database = new Database(":memory:", { nativeBinding: join(__dirname, "native", "better_sqlite3.node") });',
'const row = database.prepare("select 1 as ok").get();',
'database.close();',
'process.stdout.write(JSON.stringify({ status: row.ok === 1 ? "passed" : "failed" }));',
'',
].join("\n"));
copyTree(join(repositoryRoot, "apps", "web", "dist"), join(packageDirectory, "web"));
copyTree(join(repositoryRoot, "apps", "web", "support-gate"), join(packageDirectory, "web", "support-gate"));
writeJson(join(packageDirectory, "migrations", "manifest.json"), { migrations: [], schema_version: "0" });
writeJson(join(packageDirectory, "asset-metadata", "manifest.json"), { resources: [], schema_version: "1.0", source: "external_read_only" });
writeJson(join(packageDirectory, "LICENSES", "third-party.json"), { api: apiDependencies, runtime: { node: frozenRuntime.node }, schema_version: "1.0", worker: workerDependencies });
const commit = run("git", ["rev-parse", "HEAD"]);
writeJson(join(packageDirectory, "RELEASE.json"), {
app_version: appVersion,
browsers: [],
build_commit: commit,
release_status: "candidate_unvalidated",
schema_version: "0",
windows_build: null,
});
writeFileSync(join(packageDirectory, "START-HERE.txt"), [
"Dada P0-A candidate package",
"",
"This candidate is unsigned and is not a final P0-A release.",
"Verify the adjacent SHA-256 file before first launch.",
"Windows SmartScreen may warn on first launch because the executable is unsigned.",
"For an antivirus alert, compare the package hash with the Gitea build record.",
"Do not disable antivirus protection, add broad exclusions, or skip hash verification.",
"To update, exit Dada from the tray and replace the complete program directory.",
"Dada uses 127.0.0.1:43121 and does not support LAN or remote access.",
"A final RELEASE.json is created only after WP-7 acceptance.",
"",
].join("\r\n"));
removeTree(supervisorPublish);
const packageScan = scanPackage(packageDirectory);
if (packageScan.status !== "passed") throw new Error(`Portable package scan failed: ${JSON.stringify(packageScan.disallowed_matches)}`);
const zipPath = join(resolvedOutput, `${packageName}.zip`);
createZip(packageDirectory, zipPath);
const zipSha256 = fileSha256(zipPath);
const shaPath = `${zipPath}.sha256`;
writeFileSync(shaPath, `${zipSha256} ${basename(zipPath)}\n`);
const processTree = await verifyExtractedPackage(zipPath, packageName);
const fileEntries = listFiles(packageDirectory).files.map((path) => ({
path: relative(packageDirectory, path).replaceAll("\\", "/"),
sha256: fileSha256(path),
size: statSync(path).size,
})).sort((left, right) => left.path.localeCompare(right.path));
const finalManifest = {
app_version: appVersion,
files: fileEntries,
fixed_port: fixedPort,
package_name: packageName,
release_status: "candidate_unvalidated",
schema_version: "1.0",
zip_sha256: zipSha256,
};
if (evidenceDirectory) {
mkdirSync(evidenceDirectory, { recursive: true });
writeJson(join(evidenceDirectory, "package-manifest.json"), finalManifest);
writeFileSync(join(evidenceDirectory, "sha256.txt"), `${zipSha256} ${basename(zipPath)}\n`);
writeJson(join(evidenceDirectory, "package-scan.json"), packageScan);
writeJson(join(evidenceDirectory, "process-tree.json"), processTree);
}
return { packageManifest: finalManifest, packageScan, processTree, status: "passed" };
}
+69
View File
@@ -0,0 +1,69 @@
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
const phaseIndex = process.argv.indexOf("--phase");
const phase = phaseIndex >= 0 ? process.argv[phaseIndex + 1] : "green";
if (!["red", "green"].includes(phase)) throw new Error(`Unsupported phase: ${phase}`);
const runId = process.env.DADA_TDD_RUN_ID ?? `wp0-09-${phase}-${new Date().toISOString().replace(/[^0-9]/g, "")}`;
const runDirectory = resolve("artifacts", "tdd", runId);
const caseId = "TDD-WP0-PKG-001-portable-zip";
const caseDirectory = resolve(runDirectory, "cases", caseId);
if (existsSync(runDirectory)) throw new Error(`Evidence run already exists: ${runId}`);
mkdirSync(caseDirectory, { recursive: true });
const commandSpecs = phase === "red"
? [["node --test tests/package/wp0-09-portable.test.mjs", ["node", "--test", "tests/package/wp0-09-portable.test.mjs"]]]
: [
["pnpm test:security", ["pnpm", "test:security"]],
["pnpm test:package", ["pnpm", "test:package"]],
["pnpm validate:tdd-trace", ["pnpm", "validate:tdd-trace"]],
];
const environment = { ...process.env, DADA_EVIDENCE_DIR_PACKAGE: caseDirectory };
const startedAt = new Date().toISOString();
const commands = [];
for (const [command, invocation] of commandSpecs) {
const started_at = new Date().toISOString();
const [program, ...args] = invocation;
const executable = process.platform === "win32" ? (process.env.ComSpec ?? "cmd.exe") : program;
const actualArgs = process.platform === "win32" ? ["/d", "/s", "/c", [program, ...args].join(" ")] : args;
const execution = spawnSync(executable, actualArgs, { encoding: "utf8", env: environment });
if (execution.stdout) process.stdout.write(execution.stdout);
if (execution.stderr) process.stderr.write(execution.stderr);
commands.push({ command, exit_code: execution.status ?? 1, finished_at: new Date().toISOString(), started_at });
}
writeFileSync(resolve(caseDirectory, "commands.json"), `${JSON.stringify({ commands, phase, run_id: runId, schema_version: "1.0" }, null, 2)}\n`);
const evidenceRefs = ["package-manifest.json", "sha256.txt", "package-scan.json", "process-tree.json"];
const missingEvidence = phase === "green" ? evidenceRefs.filter((path) => !existsSync(resolve(caseDirectory, path))) : [];
const commandState = phase === "red" ? commands.every((item) => item.exit_code !== 0) : commands.every((item) => item.exit_code === 0);
const status = phase === "red" ? (commandState ? "red_confirmed" : "failed") : (commandState && missingEvidence.length === 0 ? "passed" : "failed");
const result = {
acceptance_criteria: ["AC-24", "AC-41", "AC-56"],
automation: ["automated"],
commit: spawnSync("git", ["rev-parse", "--short", "HEAD"], { encoding: "utf8" }).stdout.trim(),
environment: { arch: process.arch, node: process.version.slice(1), os: process.platform },
evidence_refs: evidenceRefs,
finished_at: new Date().toISOString(),
layer: ["PACKAGE_SECURITY"],
manifest: { path: "tasks.manifest.json", sha256: createHash("sha256").update(readFileSync("tasks.manifest.json")).digest("hex").toUpperCase() },
missing_evidence: missingEvidence,
parent_family: "TDD-WP0-PKG-001",
phase,
release_gate: ["work_package:WP-0", "release:P0-A"],
requirements: ["NFR-01", "NFR-09"],
run_id: runId,
schema_version: "1.0",
started_at: startedAt,
status,
task_id: "TASK-WP0-09",
test_id: caseId,
work_package: "WP-0",
worktree_under_test: spawnSync("git", ["status", "--porcelain"], { encoding: "utf8" }).stdout.trim() ? "uncommitted implementation" : "clean committed implementation",
};
writeFileSync(resolve(caseDirectory, "result.json"), `${JSON.stringify(result, null, 2)}\n`);
const summary = { cases: [{ missing_evidence: missingEvidence, status, test_id: caseId }], phase, run_id: runId, status };
writeFileSync(resolve(runDirectory, "evidence.json"), `${JSON.stringify(summary, null, 2)}\n`);
console.log(JSON.stringify(summary, null, 2));
if (status !== (phase === "red" ? "red_confirmed" : "passed")) process.exit(1);
+29
View File
@@ -0,0 +1,29 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
const policy = JSON.parse(readFileSync("ci/runner-isolation-policy.json", "utf8"));
const workflow = readFileSync(".gitea/workflows/p0a-ci.yml", "utf8");
for (const label of policy.required_labels) assert.match(workflow, new RegExp(`\\b${label}\\b`));
assert.match(workflow, /pnpm install --frozen-lockfile/);
assert.match(workflow, /DADA_EXTERNAL_MODE:\s*mock/);
assert.match(workflow, /pnpm test:all/);
assert.match(workflow, /pnpm package:portable/);
const forbiddenEnvironment = [
"DADA_LOCAL_DATA_ROOT",
"DADA_ASSET_ROOT",
"AI_GATEWAY_API_KEY",
"RESEND_API_KEY",
"AMAP_API_KEY",
];
const present = forbiddenEnvironment.filter((name) => Object.hasOwn(process.env, name));
assert.deepEqual(present, [], `Isolated CI received forbidden environment inputs: ${present.join(", ")}`);
console.log(JSON.stringify({
external_mode: policy.external_mode,
forbidden_environment_inputs: present,
required_labels: policy.required_labels,
schema_version: policy.schema_version,
status: "passed",
}, null, 2));
@@ -40,8 +40,8 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
}
var node = Path.Combine(AppContext.BaseDirectory, "runtime", "node.exe");
var apiEntry = Path.Combine(AppContext.BaseDirectory, "apps", "api", "dist", "main.js");
var workerEntry = Path.Combine(AppContext.BaseDirectory, "apps", "worker", "dist", "worker.js");
var apiEntry = Path.Combine(AppContext.BaseDirectory, "server", "api.mjs");
var workerEntry = Path.Combine(AppContext.BaseDirectory, "server", "worker.mjs");
if (!File.Exists(node) || !File.Exists(apiEntry) || !File.Exists(workerEntry)) return SupervisorState.StartupFailed;
api = CreateComponent(node, apiEntry, ChildRole.Api, SupervisorState.ApiDegraded);
@@ -57,6 +57,9 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
var component = new ManagedComponentSupervisor(async cancellationToken =>
{
var startInfo = new ProcessStartInfo(node);
startInfo.WorkingDirectory = AppContext.BaseDirectory;
startInfo.Environment["DADA_SQLITE_NATIVE_BINDING"] = Path.Combine(AppContext.BaseDirectory, "server", "native", "better_sqlite3.node");
startInfo.Environment["DADA_SUPPORT_GATE_ROOT"] = Path.Combine(AppContext.BaseDirectory, "web", "support-gate");
startInfo.ArgumentList.Add(entry);
var child = await ManagedChildProcess.StartAsync(startInfo, role, credentials, cancellationToken);
child.StatusReceived += status =>
+25
View File
@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { resolve } from "node:path";
import { buildAndValidatePortablePackage } from "../../scripts/lib/portable-package.mjs";
test("builds an isolated candidate portable package", async () => {
const outputRoot = resolve(".build", "wp0-09-portable-test");
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_PACKAGE
? resolve(process.env.DADA_EVIDENCE_DIR_PACKAGE)
: undefined;
const result = await buildAndValidatePortablePackage({ evidenceDirectory, outputRoot });
assert.equal(result.status, "passed");
assert.equal(result.packageManifest.fixed_port, 43121);
assert.equal(result.packageManifest.release_status, "candidate_unvalidated");
assert.equal(result.packageScan.disallowed_matches.length, 0);
assert.equal(result.packageScan.reparse_points.length, 0);
assert.equal(result.processTree.api.health.bind_scope, "loopback");
assert.equal(result.processTree.api.health.port, 43121);
assert.equal(result.processTree.api.release_gate.status_code, 426);
assert.equal(result.processTree.native.status, "passed");
assert.equal(result.processTree.supervisor.credential_store_access, false);
assert.equal(result.processTree.supervisor.status, "passed");
});