@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.9445952e33ba
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -2
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +4 -1
- package/dist/bridge/bridge.service.d.ts +72 -3
- package/dist/bridge/bridge.service.js +1563 -152
- package/dist/bridge/bridge.types.d.ts +46 -1
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +1 -0
- package/dist/cleanup/cleanup.service.js +70 -9
- package/dist/cli/cli.help.js +2 -2
- package/dist/cli/cli.service.d.ts +14 -6
- package/dist/cli/cli.service.js +75 -16
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +4 -0
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-database-broker.d.ts +89 -0
- package/dist/runtime/attempt-database-broker.js +390 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
- package/dist/runtime/attempt-process-supervisor.js +67 -20
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +62 -8
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +191 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +8 -1
- package/dist/runtime/host-listener-inspector.js +121 -60
- package/dist/runtime/host-runtime-contract.d.ts +12 -1
- package/dist/runtime/host-runtime-contract.js +168 -55
- package/dist/runtime/host-runtime-lease.d.ts +92 -8
- package/dist/runtime/host-runtime-lease.js +1141 -114
- package/dist/runtime/host-runtime-promotion.d.ts +12 -3
- package/dist/runtime/host-runtime-promotion.js +59 -12
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
- package/dist/runtime/node-host-runtime-adapter.js +28 -11
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.js +4 -1
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/preflight.d.ts +31 -1
- package/dist/sandbox/preflight.js +237 -51
- package/dist/sandbox/repository-lock.d.ts +2 -1
- package/dist/sandbox/repository-lock.js +17 -8
- package/dist/sandbox/runtime.d.ts +6 -1
- package/dist/sandbox/runtime.js +132 -103
- package/dist/sandbox/sandbox.types.d.ts +7 -0
- package/dist/sandbox/workspace.service.js +2 -0
- package/dist/session/managed-attempt-tools.d.ts +17 -1
- package/dist/session/managed-attempt-tools.js +485 -31
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +224 -22
- package/dist/session/phase-completion.d.ts +11 -1
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +30 -7
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.js +17 -18
- package/dist/session/phase-completion.runtime.d.ts +3 -1
- package/dist/session/phase-completion.runtime.js +126 -17
- package/dist/session/phase-completion.schemas.js +49 -2
- package/dist/session/phase-completion.validation.d.ts +1 -0
- package/dist/session/phase-completion.validation.js +111 -21
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +661 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
- package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
- package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
- package/dist/session/provider-cli/provider-cli.process.js +422 -22
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
- package/dist/session/provider-cli/provider-cli.service.js +773 -69
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
- package/dist/session/provider-cli/provider-output-state.js +361 -17
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.js +434 -9
- package/dist/session/session.service.js +145 -23
- package/dist/session/session.types.d.ts +147 -2
- package/dist/session/session.types.js +587 -7
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +18 -0
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-supervisor.d.ts +2 -0
- package/dist/shared/process/host-process-supervisor.js +57 -9
- package/dist/shared/process/process.d.ts +2 -0
- package/dist/shared/process/process.js +12 -7
- package/dist/shared/provider-capabilities.d.ts +3 -0
- package/dist/shared/provider-capabilities.js +85 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +39 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/dependency-warmer.d.ts +41 -9
- package/dist/worktree/dependency-warmer.js +497 -22
- package/dist/worktree/index.d.ts +1 -1
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree.service.d.ts +7 -0
- package/dist/worktree/worktree.service.js +253 -21
- package/dist/worktree/worktree.types.d.ts +7 -1
- package/package.json +7 -5
- package/runtime/windows-job-supervisor.ps1 +380 -14
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { ForgeFailureError } from "../session/session.types.js";
|
|
6
|
+
const inFlightPrebuilds = new Map();
|
|
7
|
+
export function createAspNetRuntimePrebuildPlan(options) {
|
|
8
|
+
if (!isDotnetRun(options.command, options.args))
|
|
9
|
+
return null;
|
|
10
|
+
const projectArgument = readOptionValue(options.args, "--project");
|
|
11
|
+
const projectPath = path.resolve(options.cwd, projectArgument ?? ".");
|
|
12
|
+
const normalizedWorktree = path.resolve(options.worktreePath);
|
|
13
|
+
const relativeProject = path.relative(normalizedWorktree, projectPath);
|
|
14
|
+
if (relativeProject.startsWith("..") || path.isAbsolute(relativeProject)) {
|
|
15
|
+
throw new Error("host_runtime_build_invalid: the ASP.NET project must remain inside the managed worktree.");
|
|
16
|
+
}
|
|
17
|
+
const projectDirectory = projectArgument && path.extname(projectPath).toLowerCase() === ".csproj"
|
|
18
|
+
? path.dirname(projectPath)
|
|
19
|
+
: projectPath;
|
|
20
|
+
const buildArgs = [
|
|
21
|
+
"build",
|
|
22
|
+
...(projectArgument ? [projectArgument] : []),
|
|
23
|
+
"--nologo",
|
|
24
|
+
"--no-restore",
|
|
25
|
+
"--disable-build-servers",
|
|
26
|
+
"-p:UseSharedCompilation=false",
|
|
27
|
+
"-p:DefaultItemExcludesInProjectFolder=TestResults/**",
|
|
28
|
+
...forwardBuildOptions(options.args),
|
|
29
|
+
];
|
|
30
|
+
const fingerprint = `sha256:${createHash("sha256").update(JSON.stringify({
|
|
31
|
+
inputCommitSha: options.inputCommitSha,
|
|
32
|
+
runtimeContractFingerprint: options.runtimeContractFingerprint,
|
|
33
|
+
project: relativeProject.replaceAll("\\", "/"),
|
|
34
|
+
args: buildArgs,
|
|
35
|
+
environment: buildEnvironmentContract(options.environment ?? {}),
|
|
36
|
+
})).digest("hex")}`;
|
|
37
|
+
return {
|
|
38
|
+
fingerprint,
|
|
39
|
+
receiptPath: path.join(projectDirectory, "obj", ".forge-runtime-build.json"),
|
|
40
|
+
command: options.command,
|
|
41
|
+
args: buildArgs,
|
|
42
|
+
cwd: options.cwd,
|
|
43
|
+
environment: options.environment ?? {},
|
|
44
|
+
projectPath,
|
|
45
|
+
worktreePath: normalizedWorktree,
|
|
46
|
+
inputCommitSha: options.inputCommitSha,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function ensureAspNetRuntimePrebuilt(plan, execute, signal) {
|
|
50
|
+
const pending = inFlightPrebuilds.get(plan.receiptPath);
|
|
51
|
+
if (pending)
|
|
52
|
+
return await pending;
|
|
53
|
+
const started = ensureAspNetRuntimePrebuiltCore(plan, execute, signal);
|
|
54
|
+
inFlightPrebuilds.set(plan.receiptPath, started);
|
|
55
|
+
try {
|
|
56
|
+
return await started;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (inFlightPrebuilds.get(plan.receiptPath) === started)
|
|
60
|
+
inFlightPrebuilds.delete(plan.receiptPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function ensureAspNetRuntimePrebuiltCore(plan, execute, signal) {
|
|
64
|
+
const inputs = await aspNetBuildInputs(plan);
|
|
65
|
+
const fingerprint = `sha256:${createHash("sha256").update(JSON.stringify({
|
|
66
|
+
planFingerprint: plan.fingerprint,
|
|
67
|
+
...inputs,
|
|
68
|
+
})).digest("hex")}`;
|
|
69
|
+
const receipt = await readReceipt(plan.receiptPath);
|
|
70
|
+
if (receipt?.fingerprint === fingerprint
|
|
71
|
+
&& receipt.planFingerprint === plan.fingerprint
|
|
72
|
+
&& receipt.inputCommitSha === plan.inputCommitSha
|
|
73
|
+
&& await aspNetOutputsMatch(plan, receipt.outputs)) {
|
|
74
|
+
return { cacheHit: true, receiptPath: plan.receiptPath, fingerprint };
|
|
75
|
+
}
|
|
76
|
+
const result = await execute(plan, signal);
|
|
77
|
+
if (result.exitCode !== 0) {
|
|
78
|
+
const evidence = [
|
|
79
|
+
diagnostic("stderr", result.stderrTail),
|
|
80
|
+
diagnostic("stdout", result.stdoutTail),
|
|
81
|
+
`stdout log: ${result.stdoutLogPath}`,
|
|
82
|
+
`stderr log: ${result.stderrLogPath}`,
|
|
83
|
+
].filter(Boolean).join("; ");
|
|
84
|
+
throw new ForgeFailureError("host_runtime_project_start_failed", `host_runtime_project_start_failed: dotnet build exited with code ${result.exitCode}.${evidence ? ` ${evidence}` : ""}`, "bridge");
|
|
85
|
+
}
|
|
86
|
+
await fs.mkdir(path.dirname(plan.receiptPath), { recursive: true });
|
|
87
|
+
const outputs = await collectAspNetBuildOutputs(plan);
|
|
88
|
+
if (outputs.length === 0) {
|
|
89
|
+
throw new Error("host_runtime_build_output_missing: dotnet build succeeded without producing a verifiable project output.");
|
|
90
|
+
}
|
|
91
|
+
const temporaryReceipt = `${plan.receiptPath}.${randomUUID()}.tmp`;
|
|
92
|
+
const nextReceipt = {
|
|
93
|
+
schemaVersion: 2,
|
|
94
|
+
fingerprint,
|
|
95
|
+
planFingerprint: plan.fingerprint,
|
|
96
|
+
inputCommitSha: plan.inputCommitSha,
|
|
97
|
+
projectPath: plan.projectPath,
|
|
98
|
+
...inputs,
|
|
99
|
+
outputs,
|
|
100
|
+
completedAt: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
await fs.writeFile(temporaryReceipt, `${JSON.stringify(nextReceipt)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
103
|
+
try {
|
|
104
|
+
await fs.rename(temporaryReceipt, plan.receiptPath);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await fs.rm(temporaryReceipt, { force: true }).catch(() => undefined);
|
|
108
|
+
}
|
|
109
|
+
return { cacheHit: false, receiptPath: plan.receiptPath, fingerprint };
|
|
110
|
+
}
|
|
111
|
+
export function withAspNetPrebuiltRunArguments(command, args) {
|
|
112
|
+
if (!isDotnetRun(command, args)) {
|
|
113
|
+
throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires a dotnet run command.");
|
|
114
|
+
}
|
|
115
|
+
const applicationArguments = args.indexOf("--");
|
|
116
|
+
const insertionIndex = applicationArguments < 0 ? args.length : applicationArguments;
|
|
117
|
+
const beforeApplication = args.slice(0, insertionIndex);
|
|
118
|
+
return [
|
|
119
|
+
...beforeApplication,
|
|
120
|
+
...(!beforeApplication.includes("--no-build") ? ["--no-build"] : []),
|
|
121
|
+
...(!beforeApplication.includes("--no-restore") ? ["--no-restore"] : []),
|
|
122
|
+
...args.slice(insertionIndex),
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
export async function resolveWorktreeCommitSha(worktreePath, supplied) {
|
|
126
|
+
if (supplied && /^[0-9a-f]{40,64}$/iu.test(supplied))
|
|
127
|
+
return supplied.toLowerCase();
|
|
128
|
+
return await new Promise((resolve, reject) => {
|
|
129
|
+
const child = spawn("git", ["rev-parse", "--verify", "HEAD^{commit}"], {
|
|
130
|
+
cwd: worktreePath,
|
|
131
|
+
windowsHide: true,
|
|
132
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
133
|
+
});
|
|
134
|
+
let stdout = "";
|
|
135
|
+
let stderr = "";
|
|
136
|
+
child.stdout.setEncoding("utf8");
|
|
137
|
+
child.stderr.setEncoding("utf8");
|
|
138
|
+
child.stdout.on("data", chunk => { stdout = `${stdout}${String(chunk)}`.slice(-256); });
|
|
139
|
+
child.stderr.on("data", chunk => { stderr = `${stderr}${String(chunk)}`.slice(-1_000); });
|
|
140
|
+
child.once("error", reject);
|
|
141
|
+
child.once("close", code => {
|
|
142
|
+
const commit = stdout.trim();
|
|
143
|
+
if (code === 0 && /^[0-9a-f]{40,64}$/iu.test(commit))
|
|
144
|
+
resolve(commit.toLowerCase());
|
|
145
|
+
else
|
|
146
|
+
reject(new Error(`host_runtime_build_commit_unavailable: git rev-parse failed (${code ?? "unknown"}): ${stderr.trim()}`));
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function isDotnetRun(command, args) {
|
|
151
|
+
const executable = path.basename(command).toLowerCase();
|
|
152
|
+
return (executable === "dotnet" || executable === "dotnet.exe") && args[0]?.toLowerCase() === "run";
|
|
153
|
+
}
|
|
154
|
+
function readOptionValue(args, option) {
|
|
155
|
+
const index = args.findIndex(argument => argument === option || argument.startsWith(`${option}=`));
|
|
156
|
+
if (index < 0)
|
|
157
|
+
return null;
|
|
158
|
+
const argument = args[index];
|
|
159
|
+
return argument.startsWith(`${option}=`) ? argument.slice(option.length + 1) : args[index + 1] ?? null;
|
|
160
|
+
}
|
|
161
|
+
function forwardBuildOptions(args) {
|
|
162
|
+
const forwarded = [];
|
|
163
|
+
const optionsWithValues = new Set(["-c", "--configuration", "-f", "--framework", "-r", "--runtime", "--arch", "--os"]);
|
|
164
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
165
|
+
const argument = args[index];
|
|
166
|
+
if (argument === "--")
|
|
167
|
+
break;
|
|
168
|
+
if (argument === "--project") {
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (argument.startsWith("--project="))
|
|
173
|
+
continue;
|
|
174
|
+
if (optionsWithValues.has(argument)) {
|
|
175
|
+
const value = args[index + 1];
|
|
176
|
+
if (value !== undefined) {
|
|
177
|
+
forwarded.push(argument, value);
|
|
178
|
+
index += 1;
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if ([...optionsWithValues].some(option => argument.startsWith(`${option}=`))
|
|
183
|
+
|| argument.startsWith("-p:")
|
|
184
|
+
|| argument.startsWith("/p:")) {
|
|
185
|
+
forwarded.push(argument);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return forwarded;
|
|
189
|
+
}
|
|
190
|
+
async function readReceipt(receiptPath) {
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(await fs.readFile(receiptPath, "utf8"));
|
|
193
|
+
return parsed.schemaVersion === 2
|
|
194
|
+
&& typeof parsed.fingerprint === "string"
|
|
195
|
+
&& typeof parsed.planFingerprint === "string"
|
|
196
|
+
&& typeof parsed.inputCommitSha === "string"
|
|
197
|
+
&& typeof parsed.sourceDigest === "string"
|
|
198
|
+
&& typeof parsed.dependencyDigest === "string"
|
|
199
|
+
&& typeof parsed.sdkVersion === "string"
|
|
200
|
+
&& Array.isArray(parsed.outputs)
|
|
201
|
+
? parsed
|
|
202
|
+
: null;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError)
|
|
206
|
+
return null;
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function aspNetBuildInputs(plan) {
|
|
211
|
+
const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
|
|
212
|
+
? path.dirname(plan.projectPath)
|
|
213
|
+
: plan.projectPath;
|
|
214
|
+
const sourceDigest = await directoryContentDigest(projectDirectory);
|
|
215
|
+
const dependencyReceipt = path.join(projectDirectory, "obj", ".forge-dependency-preparation.json");
|
|
216
|
+
const dependencyContent = await fs.readFile(dependencyReceipt).catch(() => null);
|
|
217
|
+
const globalJson = await findAncestorFile(projectDirectory, plan.worktreePath, "global.json");
|
|
218
|
+
const globalJsonContent = globalJson ? await fs.readFile(globalJson) : null;
|
|
219
|
+
return {
|
|
220
|
+
sourceDigest,
|
|
221
|
+
dependencyDigest: `sha256:${createHash("sha256")
|
|
222
|
+
.update(dependencyContent ?? Buffer.alloc(0))
|
|
223
|
+
.update(globalJsonContent ?? Buffer.alloc(0))
|
|
224
|
+
.digest("hex")}`,
|
|
225
|
+
sdkVersion: await dotnetSdkVersion(plan.cwd),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function directoryContentDigest(directory) {
|
|
229
|
+
const files = [];
|
|
230
|
+
await collectInputFiles(directory, directory, files);
|
|
231
|
+
const hash = createHash("sha256");
|
|
232
|
+
for (const file of files.sort((left, right) => left.localeCompare(right))) {
|
|
233
|
+
hash.update(path.relative(directory, file).replaceAll("\\", "/"));
|
|
234
|
+
hash.update("\0");
|
|
235
|
+
hash.update(await fs.readFile(file));
|
|
236
|
+
hash.update("\0");
|
|
237
|
+
}
|
|
238
|
+
return `sha256:${hash.digest("hex")}`;
|
|
239
|
+
}
|
|
240
|
+
async function collectInputFiles(root, directory, files) {
|
|
241
|
+
const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
242
|
+
for (const entry of entries) {
|
|
243
|
+
const absolute = path.join(directory, entry.name);
|
|
244
|
+
if (entry.isDirectory()) {
|
|
245
|
+
if (!["bin", "obj", "node_modules", ".git", "TestResults"].includes(entry.name)) {
|
|
246
|
+
await collectInputFiles(root, absolute, files);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (entry.isFile()) {
|
|
250
|
+
files.push(absolute);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function collectAspNetBuildOutputs(plan) {
|
|
255
|
+
const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
|
|
256
|
+
? path.dirname(plan.projectPath)
|
|
257
|
+
: plan.projectPath;
|
|
258
|
+
const outputRoot = path.join(projectDirectory, "bin");
|
|
259
|
+
const outputs = [];
|
|
260
|
+
await collectOutputFiles(outputRoot, projectDirectory, outputs);
|
|
261
|
+
return outputs.sort((left, right) => left.path.localeCompare(right.path));
|
|
262
|
+
}
|
|
263
|
+
async function collectOutputFiles(directory, projectDirectory, outputs) {
|
|
264
|
+
const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
const absolute = path.join(directory, entry.name);
|
|
267
|
+
if (entry.isDirectory())
|
|
268
|
+
await collectOutputFiles(absolute, projectDirectory, outputs);
|
|
269
|
+
else if (entry.isFile()) {
|
|
270
|
+
const stat = await fs.stat(absolute);
|
|
271
|
+
outputs.push({
|
|
272
|
+
path: path.relative(projectDirectory, absolute).replaceAll("\\", "/"),
|
|
273
|
+
size: stat.size,
|
|
274
|
+
digest: createHash("sha256").update(await fs.readFile(absolute)).digest("hex"),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function aspNetOutputsMatch(plan, outputs) {
|
|
280
|
+
if (outputs.length === 0)
|
|
281
|
+
return false;
|
|
282
|
+
const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
|
|
283
|
+
? path.dirname(plan.projectPath)
|
|
284
|
+
: plan.projectPath;
|
|
285
|
+
for (const output of outputs) {
|
|
286
|
+
const absolute = path.resolve(projectDirectory, output.path);
|
|
287
|
+
const relative = path.relative(projectDirectory, absolute);
|
|
288
|
+
if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
289
|
+
return false;
|
|
290
|
+
const stat = await fs.stat(absolute).catch(() => null);
|
|
291
|
+
if (!stat?.isFile() || stat.size !== output.size)
|
|
292
|
+
return false;
|
|
293
|
+
const digest = createHash("sha256").update(await fs.readFile(absolute)).digest("hex");
|
|
294
|
+
if (digest !== output.digest)
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
async function findAncestorFile(directory, root, name) {
|
|
300
|
+
let current = path.resolve(directory);
|
|
301
|
+
const resolvedRoot = path.resolve(root);
|
|
302
|
+
while (true) {
|
|
303
|
+
const candidate = path.join(current, name);
|
|
304
|
+
if (await fs.stat(candidate).then(stat => stat.isFile()).catch(() => false))
|
|
305
|
+
return candidate;
|
|
306
|
+
if (current === resolvedRoot)
|
|
307
|
+
return null;
|
|
308
|
+
const parent = path.dirname(current);
|
|
309
|
+
if (parent === current || path.relative(resolvedRoot, parent).startsWith(".."))
|
|
310
|
+
return null;
|
|
311
|
+
current = parent;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function dotnetSdkVersion(cwd) {
|
|
315
|
+
return await new Promise(resolve => {
|
|
316
|
+
const child = spawn("dotnet", ["--version"], { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
317
|
+
let output = "";
|
|
318
|
+
child.stdout.setEncoding("utf8");
|
|
319
|
+
child.stderr.setEncoding("utf8");
|
|
320
|
+
child.stdout.on("data", chunk => { output += String(chunk); });
|
|
321
|
+
child.stderr.on("data", chunk => { output += String(chunk); });
|
|
322
|
+
child.once("error", () => resolve("unavailable"));
|
|
323
|
+
child.once("close", code => resolve(code === 0 ? output.trim().slice(0, 256) : `unavailable:${code ?? "unknown"}`));
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function buildEnvironmentContract(environment) {
|
|
327
|
+
return {
|
|
328
|
+
DOTNET_ROOT: environment.DOTNET_ROOT ?? process.env.DOTNET_ROOT ?? null,
|
|
329
|
+
DOTNET_ROLL_FORWARD: environment.DOTNET_ROLL_FORWARD ?? process.env.DOTNET_ROLL_FORWARD ?? null,
|
|
330
|
+
DOTNET_MULTILEVEL_LOOKUP: environment.DOTNET_MULTILEVEL_LOOKUP ?? process.env.DOTNET_MULTILEVEL_LOOKUP ?? null,
|
|
331
|
+
MSBuildSDKsPath: environment.MSBuildSDKsPath ?? process.env.MSBuildSDKsPath ?? null,
|
|
332
|
+
NUGET_PACKAGES: environment.NUGET_PACKAGES ?? process.env.NUGET_PACKAGES ?? null,
|
|
333
|
+
FORGE_BUILD_CACHE_EPOCH: createHash("sha256")
|
|
334
|
+
.update(environment.FORGE_BUILD_CACHE_EPOCH ?? process.env.FORGE_BUILD_CACHE_EPOCH ?? "default")
|
|
335
|
+
.digest("hex"),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function diagnostic(label, value) {
|
|
339
|
+
const normalized = value.replaceAll(/[\r\n\t]+/gu, " ").trim();
|
|
340
|
+
return normalized ? `${label} tail: ${normalized.slice(-2_000)}` : "";
|
|
341
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { NativeTestPostgresFallback } from "./native-test-postgres-fallback.js";
|
|
2
|
+
/**
|
|
3
|
+
* The managed host attempt's database capability. The Bridge keeps the test cluster's
|
|
4
|
+
* administrator credential; an attempt (its agent, its application runtime and its tests)
|
|
5
|
+
* receives only a login for one database created for it. Forge.Api checks that login before it
|
|
6
|
+
* migrates (AttemptScopedDatabasePolicy.AssertBrokeredCapabilityAsync), but that check runs in
|
|
7
|
+
* attempt code: the boundary is what this broker issues and the cluster's pg_hba rules, which
|
|
8
|
+
* let an attempt role log in only to its own database.
|
|
9
|
+
*/
|
|
10
|
+
export declare const attemptDatabaseCapabilityEnvironment = "FORGE_ATTEMPT_DATABASE_CAPABILITY";
|
|
11
|
+
export interface AttemptDatabaseCapability {
|
|
12
|
+
readonly databaseName: string;
|
|
13
|
+
readonly connectionString: string;
|
|
14
|
+
}
|
|
15
|
+
export type AttemptDatabaseReleaseOutcome = "dropped" | "missing" | "retained";
|
|
16
|
+
export interface AttemptDatabaseBroker {
|
|
17
|
+
provision(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal): Promise<AttemptDatabaseCapability>;
|
|
18
|
+
release(dispatchAttemptId: string, fencingToken: string): Promise<AttemptDatabaseReleaseOutcome>;
|
|
19
|
+
/**
|
|
20
|
+
* Drops this broker's databases whose attempt is no longer live, asking `isLive` about each one
|
|
21
|
+
* just before acting on it. Returns the dropped names.
|
|
22
|
+
*/
|
|
23
|
+
reapOrphans(isLive: (dispatchAttemptId: string) => boolean): Promise<readonly string[]>;
|
|
24
|
+
}
|
|
25
|
+
export declare class AttemptDatabaseBrokerError extends Error {
|
|
26
|
+
readonly code: string;
|
|
27
|
+
constructor(code: string, detail: string);
|
|
28
|
+
}
|
|
29
|
+
export interface AttemptDatabaseLogin {
|
|
30
|
+
readonly database: string;
|
|
31
|
+
readonly username: string;
|
|
32
|
+
readonly password: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Runs one psql script and returns its unaligned output: as the cluster administrator against
|
|
36
|
+
* `postgres`, or, given a login, as that login against its database.
|
|
37
|
+
*/
|
|
38
|
+
export type AttemptDatabaseSqlRunner = (sql: string, signal?: AbortSignal, login?: AttemptDatabaseLogin) => Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* The ownership marker on the attempt's role. Only a role with CREATEROLE can change a role's
|
|
41
|
+
* comment, so the attempt cannot forge or erase it (it can rewrite its database's comment, which
|
|
42
|
+
* is therefore informational). Must match AttemptScopedDatabasePolicy.BrokeredRoleMarkerPrefix.
|
|
43
|
+
*/
|
|
44
|
+
export declare function brokeredDatabaseMarker(dispatchAttemptId: string, fencingToken: string, owner: string): string;
|
|
45
|
+
export declare function attemptDatabaseName(dispatchAttemptId: string): string;
|
|
46
|
+
/** Identifies one Bridge's broker on a shared cluster, so it reaps only databases it issued. */
|
|
47
|
+
export declare function attemptDatabaseBrokerOwner(stateRoot: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Brokers attempt databases on the dedicated native test cluster that
|
|
50
|
+
* scripts/setup-native-test-postgres.ps1 provisions. That cluster holds only disposable test
|
|
51
|
+
* databases, so even a leaked attempt capability cannot reach developer data. Each attempt
|
|
52
|
+
* gets a login role and a database it owns, cloned from template1 (which carries pgvector,
|
|
53
|
+
* an extension an unprivileged owner cannot create).
|
|
54
|
+
*/
|
|
55
|
+
export declare class NativeTestClusterDatabaseBroker implements AttemptDatabaseBroker {
|
|
56
|
+
private readonly admin;
|
|
57
|
+
private readonly runSql;
|
|
58
|
+
private readonly owner;
|
|
59
|
+
private readonly delay;
|
|
60
|
+
/** Names being provisioned right now; the reaper leaves them alone. */
|
|
61
|
+
private readonly provisioning;
|
|
62
|
+
constructor(admin: NativeTestClusterAdmin, runSql: AttemptDatabaseSqlRunner, owner: string, delay?: (milliseconds: number) => Promise<void>);
|
|
63
|
+
provision(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal): Promise<AttemptDatabaseCapability>;
|
|
64
|
+
release(dispatchAttemptId: string, fencingToken: string): Promise<AttemptDatabaseReleaseOutcome>;
|
|
65
|
+
reapOrphans(isLive: (dispatchAttemptId: string) => boolean): Promise<readonly string[]>;
|
|
66
|
+
private state;
|
|
67
|
+
private createDatabase;
|
|
68
|
+
/** Fails closed unless the cluster lets this login reach its own database and nothing else. */
|
|
69
|
+
private assertConfinedToOwnDatabase;
|
|
70
|
+
private drop;
|
|
71
|
+
}
|
|
72
|
+
export interface NativeTestClusterAdmin {
|
|
73
|
+
readonly host: string;
|
|
74
|
+
readonly port: number;
|
|
75
|
+
readonly username: string;
|
|
76
|
+
readonly password: string;
|
|
77
|
+
}
|
|
78
|
+
export declare function nativeTestClusterAdmin(fallback: NativeTestPostgresFallback): NativeTestClusterAdmin;
|
|
79
|
+
/** Runs SQL through the psql that ships with the PostgreSQL install hosting the native test cluster. */
|
|
80
|
+
export declare function psqlSqlRunner(admin: NativeTestClusterAdmin, psqlPath: string, timeoutMs?: number): AttemptDatabaseSqlRunner;
|
|
81
|
+
/** psql beside the cluster's pg_ctl: FORGE_NATIVE_TEST_POSTGRES_PSQL, a Windows install, or PATH. */
|
|
82
|
+
export declare function resolvePsqlPath(): string | null;
|
|
83
|
+
/**
|
|
84
|
+
* The broker for this host, or null when no native test cluster is provisioned. Without a
|
|
85
|
+
* broker, Forge.Api still refuses to start in a managed host attempt; other applications run.
|
|
86
|
+
*/
|
|
87
|
+
export declare function createNativeTestClusterDatabaseBroker(readFallback: () => NativeTestPostgresFallback | null, stateRoot: string): AttemptDatabaseBroker | null;
|
|
88
|
+
/** The exact strings to redact wherever a capability may be printed: the whole value and its password. */
|
|
89
|
+
export declare function attemptDatabaseCapabilitySecrets(capability: string | undefined): readonly string[];
|