@agent-relay/factory 0.1.49 → 0.1.51
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 +93 -11
- package/dist/cli/fleet.d.ts +4 -1
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +87 -42
- package/dist/cli/fleet.js.map +1 -1
- package/dist/config/schema.d.ts +1671 -10
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +83 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/dispatch/templates.d.ts +6 -0
- package/dist/dispatch/templates.d.ts.map +1 -1
- package/dist/dispatch/templates.js +16 -0
- package/dist/dispatch/templates.js.map +1 -1
- package/dist/environments/connection-registry.d.ts +617 -0
- package/dist/environments/connection-registry.d.ts.map +1 -0
- package/dist/environments/connection-registry.js +174 -0
- package/dist/environments/connection-registry.js.map +1 -0
- package/dist/environments/index.d.ts +21 -0
- package/dist/environments/index.d.ts.map +1 -0
- package/dist/environments/index.js +11 -0
- package/dist/environments/index.js.map +1 -0
- package/dist/environments/k6-job.d.ts +64 -0
- package/dist/environments/k6-job.d.ts.map +1 -0
- package/dist/environments/k6-job.js +376 -0
- package/dist/environments/k6-job.js.map +1 -0
- package/dist/environments/kubernetes-client.d.ts +72 -0
- package/dist/environments/kubernetes-client.d.ts.map +1 -0
- package/dist/environments/kubernetes-client.js +337 -0
- package/dist/environments/kubernetes-client.js.map +1 -0
- package/dist/environments/kubernetes-command.d.ts +30 -0
- package/dist/environments/kubernetes-command.d.ts.map +1 -0
- package/dist/environments/kubernetes-command.js +108 -0
- package/dist/environments/kubernetes-command.js.map +1 -0
- package/dist/environments/kubernetes-environment.d.ts +38 -0
- package/dist/environments/kubernetes-environment.d.ts.map +1 -0
- package/dist/environments/kubernetes-environment.js +333 -0
- package/dist/environments/kubernetes-environment.js.map +1 -0
- package/dist/environments/kubernetes-provider.d.ts +83 -0
- package/dist/environments/kubernetes-provider.d.ts.map +1 -0
- package/dist/environments/kubernetes-provider.js +956 -0
- package/dist/environments/kubernetes-provider.js.map +1 -0
- package/dist/environments/load-harness.d.ts +199 -0
- package/dist/environments/load-harness.d.ts.map +1 -0
- package/dist/environments/load-harness.js +259 -0
- package/dist/environments/load-harness.js.map +1 -0
- package/dist/environments/load-profile.d.ts +294 -0
- package/dist/environments/load-profile.d.ts.map +1 -0
- package/dist/environments/load-profile.js +125 -0
- package/dist/environments/load-profile.js.map +1 -0
- package/dist/environments/stack-deployer.d.ts +22 -0
- package/dist/environments/stack-deployer.d.ts.map +1 -0
- package/dist/environments/stack-deployer.js +27 -0
- package/dist/environments/stack-deployer.js.map +1 -0
- package/dist/environments/stack-descriptor.d.ts +243 -0
- package/dist/environments/stack-descriptor.d.ts.map +1 -0
- package/dist/environments/stack-descriptor.js +88 -0
- package/dist/environments/stack-descriptor.js.map +1 -0
- package/dist/environments/verification-pipeline.d.ts +117 -0
- package/dist/environments/verification-pipeline.d.ts.map +1 -0
- package/dist/environments/verification-pipeline.js +643 -0
- package/dist/environments/verification-pipeline.js.map +1 -0
- package/dist/environments/verification-stack-deployer.d.ts +73 -0
- package/dist/environments/verification-stack-deployer.d.ts.map +1 -0
- package/dist/environments/verification-stack-deployer.js +484 -0
- package/dist/environments/verification-stack-deployer.js.map +1 -0
- package/dist/environments/verification-stack-descriptor.d.ts +1134 -0
- package/dist/environments/verification-stack-descriptor.d.ts.map +1 -0
- package/dist/environments/verification-stack-descriptor.js +275 -0
- package/dist/environments/verification-stack-descriptor.js.map +1 -0
- package/dist/environments/verification-stack.d.ts +29 -0
- package/dist/environments/verification-stack.d.ts.map +1 -0
- package/dist/environments/verification-stack.js +43 -0
- package/dist/environments/verification-stack.js.map +1 -0
- package/dist/featuremap/generate.d.ts +72 -0
- package/dist/featuremap/generate.d.ts.map +1 -0
- package/dist/featuremap/generate.js +854 -0
- package/dist/featuremap/generate.js.map +1 -0
- package/dist/featuremap/index.d.ts +2 -0
- package/dist/featuremap/index.d.ts.map +1 -1
- package/dist/featuremap/index.js +1 -0
- package/dist/featuremap/index.js.map +1 -1
- package/dist/featuremap/validate.d.ts +6 -0
- package/dist/featuremap/validate.d.ts.map +1 -1
- package/dist/featuremap/validate.js +95 -13
- package/dist/featuremap/validate.js.map +1 -1
- package/dist/fleet/create-fleet.d.ts +2 -0
- package/dist/fleet/create-fleet.d.ts.map +1 -1
- package/dist/fleet/create-fleet.js +1 -0
- package/dist/fleet/create-fleet.js.map +1 -1
- package/dist/fleet/internal-fleet-client.d.ts +8 -1
- package/dist/fleet/internal-fleet-client.d.ts.map +1 -1
- package/dist/fleet/internal-fleet-client.js +31 -6
- package/dist/fleet/internal-fleet-client.js.map +1 -1
- package/dist/fleet/relay-fleet-client.d.ts +4 -1
- package/dist/fleet/relay-fleet-client.d.ts.map +1 -1
- package/dist/fleet/relay-fleet-client.js +142 -1
- package/dist/fleet/relay-fleet-client.js.map +1 -1
- package/dist/git/agent-worktree.d.ts +4 -1
- package/dist/git/agent-worktree.d.ts.map +1 -1
- package/dist/git/agent-worktree.js +151 -8
- package/dist/git/agent-worktree.js.map +1 -1
- package/dist/index.d.ts +32 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/mount/local-mount-preflight.d.ts.map +1 -1
- package/dist/mount/local-mount-preflight.js +6 -2
- package/dist/mount/local-mount-preflight.js.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts +8 -2
- package/dist/mount/relayfile-cloud-mount-client.d.ts.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.js +11 -1
- package/dist/mount/relayfile-cloud-mount-client.js.map +1 -1
- package/dist/node/factory-node.d.ts +2 -0
- package/dist/node/factory-node.d.ts.map +1 -1
- package/dist/node/factory-node.js +107 -2
- package/dist/node/factory-node.js.map +1 -1
- package/dist/node/preview-process.d.ts +65 -0
- package/dist/node/preview-process.d.ts.map +1 -0
- package/dist/node/preview-process.js +431 -0
- package/dist/node/preview-process.js.map +1 -0
- package/dist/node/tailscale-preview.d.ts +45 -0
- package/dist/node/tailscale-preview.d.ts.map +1 -0
- package/dist/node/tailscale-preview.js +748 -0
- package/dist/node/tailscale-preview.js.map +1 -0
- package/dist/observability/events.d.ts +1838 -28
- package/dist/observability/events.d.ts.map +1 -1
- package/dist/observability/events.js +52 -0
- package/dist/observability/events.js.map +1 -1
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +1416 -78
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/orchestrator/index.d.ts +2 -1
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/reaper.d.ts +24 -0
- package/dist/orchestrator/reaper.d.ts.map +1 -1
- package/dist/orchestrator/reaper.js +117 -0
- package/dist/orchestrator/reaper.js.map +1 -1
- package/dist/ports/environment.d.ts +83 -0
- package/dist/ports/environment.d.ts.map +1 -0
- package/dist/ports/environment.js +2 -0
- package/dist/ports/environment.js.map +1 -0
- package/dist/ports/fleet.d.ts +68 -1
- package/dist/ports/fleet.d.ts.map +1 -1
- package/dist/ports/index.d.ts +3 -2
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/mount.d.ts +9 -0
- package/dist/ports/mount.d.ts.map +1 -1
- package/dist/ports/state.d.ts +17 -1
- package/dist/ports/state.d.ts.map +1 -1
- package/dist/ports/worktree.d.ts +14 -0
- package/dist/ports/worktree.d.ts.map +1 -1
- package/dist/state/file-state-store.js +28 -1
- package/dist/state/file-state-store.js.map +1 -1
- package/dist/subscriptions/index.d.ts +2 -0
- package/dist/subscriptions/index.d.ts.map +1 -1
- package/dist/subscriptions/index.js +1 -0
- package/dist/subscriptions/index.js.map +1 -1
- package/dist/subscriptions/resource-subscriptions.d.ts +77 -0
- package/dist/subscriptions/resource-subscriptions.d.ts.map +1 -0
- package/dist/subscriptions/resource-subscriptions.js +91 -0
- package/dist/subscriptions/resource-subscriptions.js.map +1 -0
- package/dist/testing/fakes.d.ts +9 -1
- package/dist/testing/fakes.d.ts.map +1 -1
- package/dist/testing/fakes.js +40 -0
- package/dist/testing/fakes.js.map +1 -1
- package/dist/triage/schema.d.ts +32 -32
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -4
- package/schemas/kubernetes-environment-stack.schema.json +86 -0
- package/schemas/verification-stack.schema.json +292 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { normalizeLogger } from '../logging.js';
|
|
8
|
+
import { createFactoryCloudEventV1 } from '../observability/events.js';
|
|
9
|
+
import { runLoad } from './load-harness.js';
|
|
10
|
+
import { loadLoadProfile } from './load-profile.js';
|
|
11
|
+
import { KubectlEnvironmentProvider } from './kubernetes-environment.js';
|
|
12
|
+
import { CommandExecutionError, ProcessCommandRunner, kubectlConnectionArgs, } from './kubernetes-command.js';
|
|
13
|
+
import { VerificationStackDeployer, } from './verification-stack-deployer.js';
|
|
14
|
+
import { loadVerificationGateStack } from './verification-stack.js';
|
|
15
|
+
export const VERIFICATION_EVIDENCE_CONTRACT = 'factory.verification.evidence.v1';
|
|
16
|
+
export const DEFAULT_VERIFICATION_DESCRIPTOR = '.factory/verification-stack.yaml';
|
|
17
|
+
export class VerificationPipeline {
|
|
18
|
+
#descriptorPath;
|
|
19
|
+
#environmentProvider;
|
|
20
|
+
#stackDeployer;
|
|
21
|
+
#e2eRunner;
|
|
22
|
+
#loadRunner;
|
|
23
|
+
#revisionResolver;
|
|
24
|
+
#reporter;
|
|
25
|
+
#logger;
|
|
26
|
+
#maxConcurrentEnvironments;
|
|
27
|
+
#maxRunTimeoutMs;
|
|
28
|
+
#maxEnvironmentTtlMs;
|
|
29
|
+
#maxTeardownTimeoutMs;
|
|
30
|
+
#now;
|
|
31
|
+
#runId;
|
|
32
|
+
#semaphore;
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.#descriptorPath = options.descriptorPath ?? DEFAULT_VERIFICATION_DESCRIPTOR;
|
|
35
|
+
this.#environmentProvider = options.environmentProvider ?? new KubectlEnvironmentProvider();
|
|
36
|
+
this.#stackDeployer = options.stackDeployer ?? new VerificationStackDeployer();
|
|
37
|
+
this.#e2eRunner = options.e2eRunner ?? runE2eCommand;
|
|
38
|
+
this.#loadRunner = options.loadRunner ?? defaultLoadRunner;
|
|
39
|
+
this.#revisionResolver = options.revisionResolver ?? resolveGitHeadRevision;
|
|
40
|
+
this.#reporter = options.reporter;
|
|
41
|
+
this.#logger = normalizeLogger(options.logger ?? console);
|
|
42
|
+
this.#maxConcurrentEnvironments = positiveInteger(options.maxConcurrentEnvironments ?? 2, 'maxConcurrentEnvironments');
|
|
43
|
+
this.#maxRunTimeoutMs = positiveInteger(options.maxRunTimeoutMs ?? 30 * 60_000, 'maxRunTimeoutMs');
|
|
44
|
+
this.#maxEnvironmentTtlMs = positiveInteger(options.maxEnvironmentTtlMs ?? 60 * 60_000, 'maxEnvironmentTtlMs');
|
|
45
|
+
this.#maxTeardownTimeoutMs = positiveInteger(options.maxTeardownTimeoutMs ?? 5 * 60_000, 'maxTeardownTimeoutMs');
|
|
46
|
+
this.#now = options.now ?? (() => new Date());
|
|
47
|
+
this.#runId = options.runId ?? randomUUID;
|
|
48
|
+
this.#semaphore = new Semaphore(this.#maxConcurrentEnvironments);
|
|
49
|
+
}
|
|
50
|
+
async verify(input) {
|
|
51
|
+
const runId = dnsLabel(input.runId ?? this.#runId()).slice(0, 24);
|
|
52
|
+
const startedAt = this.#now();
|
|
53
|
+
const stages = emptyStages();
|
|
54
|
+
const descriptorPath = input.descriptorPath ?? this.#descriptorPath;
|
|
55
|
+
let stack;
|
|
56
|
+
let environment;
|
|
57
|
+
let deployedEnvironment;
|
|
58
|
+
let deployment;
|
|
59
|
+
let release;
|
|
60
|
+
let functionalPass = false;
|
|
61
|
+
let timedOut = false;
|
|
62
|
+
let reason = 'verification did not run';
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
let overallTimer;
|
|
65
|
+
const armOverallTimeout = (timeoutMs) => {
|
|
66
|
+
if (overallTimer)
|
|
67
|
+
clearTimeout(overallTimer);
|
|
68
|
+
overallTimer = setTimeout(() => {
|
|
69
|
+
timedOut = true;
|
|
70
|
+
controller.abort(new VerificationTimeoutError(`verification timed out after ${timeoutMs}ms`));
|
|
71
|
+
}, timeoutMs);
|
|
72
|
+
};
|
|
73
|
+
armOverallTimeout(this.#maxRunTimeoutMs);
|
|
74
|
+
try {
|
|
75
|
+
const resolveStarted = Date.now();
|
|
76
|
+
try {
|
|
77
|
+
stack = await withinTimeout(loadVerificationGateStack(input.repositoryPath, descriptorPath), this.#maxRunTimeoutMs, 'verification resolution', controller.signal);
|
|
78
|
+
if (input.expectedHeadSha) {
|
|
79
|
+
const checkoutHeadSha = await this.#revisionResolver(stack.repositoryPath);
|
|
80
|
+
if (checkoutHeadSha.toLowerCase() !== input.expectedHeadSha.toLowerCase()) {
|
|
81
|
+
throw new Error(`feature checkout head mismatch: expected ${input.expectedHeadSha}, found ${checkoutHeadSha}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
stages.resolve = passStage(resolveStarted);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
stages.resolve = failStage(resolveStarted, error);
|
|
88
|
+
reason = `verification resolution failed: ${message(error)}`;
|
|
89
|
+
return await this.#finish(input, runId, startedAt, descriptorPath, stages, false, reason, undefined, controller.signal.aborted || error instanceof VerificationTimeoutError);
|
|
90
|
+
}
|
|
91
|
+
const overallMs = Math.min(stack.timeouts.overallMs, this.#maxRunTimeoutMs);
|
|
92
|
+
const remainingOverallMs = Math.max(1, overallMs - (Date.now() - startedAt.getTime()));
|
|
93
|
+
armOverallTimeout(remainingOverallMs);
|
|
94
|
+
const acquireStarted = Date.now();
|
|
95
|
+
try {
|
|
96
|
+
release = await this.#semaphore.acquire(controller.signal);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
stages.provision = timeoutOrFailure(acquireStarted, error, controller.signal);
|
|
100
|
+
reason = controller.signal.aborted ? 'verification timed out waiting for environment capacity' : message(error);
|
|
101
|
+
return await this.#finish(input, runId, startedAt, stack.descriptorPath, stages, false, reason, undefined, controller.signal.aborted);
|
|
102
|
+
}
|
|
103
|
+
const provisionStarted = Date.now();
|
|
104
|
+
try {
|
|
105
|
+
const provisionInput = {
|
|
106
|
+
runId,
|
|
107
|
+
repository: input.repository,
|
|
108
|
+
namespacePrefix: 'factory-verification',
|
|
109
|
+
ttlMs: Math.min(stack.environmentTtlMs, this.#maxEnvironmentTtlMs),
|
|
110
|
+
maxActiveEnvironments: this.#maxConcurrentEnvironments,
|
|
111
|
+
signal: controller.signal,
|
|
112
|
+
};
|
|
113
|
+
environment = await this.#environmentProvider.provision(provisionInput);
|
|
114
|
+
stages.provision = passStage(provisionStarted);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
stages.provision = timeoutOrFailure(provisionStarted, error, controller.signal);
|
|
118
|
+
reason = stageReason('provision', error, controller.signal);
|
|
119
|
+
return await this.#finishWithTeardown(input, runId, startedAt, stack, stages, false, reason, environment, deployment, controller.signal.aborted);
|
|
120
|
+
}
|
|
121
|
+
const deployStarted = Date.now();
|
|
122
|
+
try {
|
|
123
|
+
deployment = await withinTimeout(this.#stackDeployer.deploy(stack.loaded, environment, { signal: controller.signal }), remainingTimeout(stack, this.#maxRunTimeoutMs, startedAt), 'deploy stage', controller.signal);
|
|
124
|
+
deployedEnvironment = asVerificationEnvironment(environment, stack, deployment.endpoints);
|
|
125
|
+
stages.deploy = passStage(deployStarted);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
stages.deploy = timeoutOrFailure(deployStarted, error, controller.signal);
|
|
129
|
+
reason = stageReason('deploy', error, controller.signal);
|
|
130
|
+
return await this.#finishWithTeardown(input, runId, startedAt, stack, stages, false, reason, environment, deployment, controller.signal.aborted);
|
|
131
|
+
}
|
|
132
|
+
const e2eStarted = Date.now();
|
|
133
|
+
try {
|
|
134
|
+
const result = await withinTimeout(this.#e2eRunner({
|
|
135
|
+
image: stack.e2e.image,
|
|
136
|
+
environment: deployedEnvironment,
|
|
137
|
+
command: stack.e2e.command,
|
|
138
|
+
args: stack.e2e.args,
|
|
139
|
+
cwd: stack.repositoryPath,
|
|
140
|
+
env: {
|
|
141
|
+
...stack.e2e.env,
|
|
142
|
+
FACTORY_VERIFICATION_RUN_ID: runId,
|
|
143
|
+
FACTORY_ENVIRONMENT_ID: deployedEnvironment.id,
|
|
144
|
+
FACTORY_ENVIRONMENT_NAMESPACE: deployedEnvironment.namespace,
|
|
145
|
+
...endpointEnvironment(deployedEnvironment),
|
|
146
|
+
},
|
|
147
|
+
timeoutMs: stack.e2e.timeoutMs,
|
|
148
|
+
signal: controller.signal,
|
|
149
|
+
}), stack.e2e.timeoutMs, 'E2E stage', controller.signal);
|
|
150
|
+
stages.e2e = {
|
|
151
|
+
status: result.exitCode === 0 ? 'pass' : 'fail',
|
|
152
|
+
durationMs: result.durationMs,
|
|
153
|
+
exitCode: result.exitCode,
|
|
154
|
+
...(result.exitCode === 0 ? {} : { error: result.stderr.trim().slice(-4_000) || `exited with ${result.exitCode}` }),
|
|
155
|
+
};
|
|
156
|
+
if (result.exitCode !== 0) {
|
|
157
|
+
reason = `E2E stage failed with exit code ${result.exitCode}`;
|
|
158
|
+
return await this.#finishWithTeardown(input, runId, startedAt, stack, stages, false, reason, environment, deployment, false);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
stages.e2e = timeoutOrFailure(e2eStarted, error, controller.signal);
|
|
163
|
+
reason = stageReason('E2E', error, controller.signal);
|
|
164
|
+
return await this.#finishWithTeardown(input, runId, startedAt, stack, stages, false, reason, environment, deployment, controller.signal.aborted || error instanceof VerificationTimeoutError);
|
|
165
|
+
}
|
|
166
|
+
const loadStarted = Date.now();
|
|
167
|
+
try {
|
|
168
|
+
const result = await withinTimeout(this.#loadRunner(deployedEnvironment, stack, runId, controller.signal), stack.load.timeoutMs, 'load stage', controller.signal);
|
|
169
|
+
stages.load = {
|
|
170
|
+
status: result.status,
|
|
171
|
+
durationMs: elapsed(loadStarted),
|
|
172
|
+
measured: result.measured,
|
|
173
|
+
violations: result.violations,
|
|
174
|
+
...(result.status === 'pass' ? {} : { error: 'load SLO thresholds were violated' }),
|
|
175
|
+
};
|
|
176
|
+
if (result.status === 'fail') {
|
|
177
|
+
reason = 'load stage violated one or more SLO thresholds';
|
|
178
|
+
return await this.#finishWithTeardown(input, runId, startedAt, stack, stages, false, reason, environment, deployment, false);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
stages.load = timeoutOrFailure(loadStarted, error, controller.signal);
|
|
183
|
+
reason = stageReason('load', error, controller.signal);
|
|
184
|
+
return await this.#finishWithTeardown(input, runId, startedAt, stack, stages, false, reason, environment, deployment, controller.signal.aborted || error instanceof VerificationTimeoutError);
|
|
185
|
+
}
|
|
186
|
+
functionalPass = true;
|
|
187
|
+
reason = 'provision, deploy, E2E, and load SLO verification passed';
|
|
188
|
+
return await this.#finishWithTeardown(input, runId, startedAt, stack, stages, functionalPass, reason, environment, deployment, timedOut);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
if (overallTimer)
|
|
192
|
+
clearTimeout(overallTimer);
|
|
193
|
+
release?.();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async #finishWithTeardown(input, runId, startedAt, stack, stages, functionalPass, reason, environment, deployment, timedOut) {
|
|
197
|
+
const evaluateStarted = Date.now();
|
|
198
|
+
stages.evaluate = {
|
|
199
|
+
status: functionalPass ? 'pass' : 'fail',
|
|
200
|
+
durationMs: elapsed(evaluateStarted),
|
|
201
|
+
...(functionalPass ? {} : { error: reason }),
|
|
202
|
+
};
|
|
203
|
+
if (environment) {
|
|
204
|
+
const teardownStarted = Date.now();
|
|
205
|
+
const teardownController = new AbortController();
|
|
206
|
+
const teardownMs = Math.min(stack.timeouts.teardownMs, this.#maxTeardownTimeoutMs);
|
|
207
|
+
const teardownDeadline = Date.now() + teardownMs;
|
|
208
|
+
try {
|
|
209
|
+
const errors = [];
|
|
210
|
+
try {
|
|
211
|
+
await withinTimeout(deployment?.dispose() ?? Promise.resolve(), Math.max(1, Math.min(5_000, Math.floor(teardownMs / 4))), 'deployment cleanup');
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
errors.push(error);
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
await withinTimeout(this.#environmentProvider.teardown(environment, { signal: teardownController.signal }), Math.max(1, teardownDeadline - Date.now()), 'environment teardown');
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
errors.push(error);
|
|
221
|
+
}
|
|
222
|
+
if (errors.length > 0)
|
|
223
|
+
throw new AggregateError(errors, errors.map(message).join('; '));
|
|
224
|
+
stages.teardown = passStage(teardownStarted);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
teardownController.abort(error);
|
|
228
|
+
stages.teardown = error instanceof VerificationTimeoutError
|
|
229
|
+
? { status: 'timed_out', durationMs: elapsed(teardownStarted), error: message(error) }
|
|
230
|
+
: failStage(teardownStarted, error);
|
|
231
|
+
functionalPass = false;
|
|
232
|
+
reason = `verification teardown failed; environment reaper retained responsibility: ${message(error)}`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
stages.teardown = { status: 'skipped', durationMs: 0 };
|
|
237
|
+
}
|
|
238
|
+
return await this.#finish(input, runId, startedAt, stack.descriptorPath, stages, functionalPass, reason, environment, timedOut);
|
|
239
|
+
}
|
|
240
|
+
async #finish(input, runId, startedAt, descriptorPath, stages, passed, reason, environment, timedOut) {
|
|
241
|
+
if (stages.evaluate.status === 'skipped') {
|
|
242
|
+
stages.evaluate = { status: 'fail', durationMs: 0, error: reason };
|
|
243
|
+
}
|
|
244
|
+
const evidence = {
|
|
245
|
+
contract: VERIFICATION_EVIDENCE_CONTRACT,
|
|
246
|
+
runId,
|
|
247
|
+
repository: input.repository,
|
|
248
|
+
repositoryPath: input.repositoryPath,
|
|
249
|
+
descriptorPath,
|
|
250
|
+
...(input.expectedHeadSha ? { expectedHeadSha: input.expectedHeadSha } : {}),
|
|
251
|
+
...(environment ? {
|
|
252
|
+
environmentId: environment.id,
|
|
253
|
+
...(environment.namespace ? { namespace: environment.namespace } : {}),
|
|
254
|
+
} : {}),
|
|
255
|
+
startedAt: startedAt.toISOString(),
|
|
256
|
+
completedAt: this.#now().toISOString(),
|
|
257
|
+
timedOut,
|
|
258
|
+
stages,
|
|
259
|
+
};
|
|
260
|
+
const verdict = { status: passed ? 'pass' : 'fail', passed, reason, evidence };
|
|
261
|
+
if (this.#reporter) {
|
|
262
|
+
try {
|
|
263
|
+
await this.#reporter.report(createFactoryCloudEventV1({
|
|
264
|
+
type: 'verification.completed',
|
|
265
|
+
level: passed ? 'info' : 'error',
|
|
266
|
+
runId,
|
|
267
|
+
phase: 'verification',
|
|
268
|
+
status: passed ? 'succeeded' : 'failed',
|
|
269
|
+
run: {
|
|
270
|
+
source: 'verification-gate',
|
|
271
|
+
repository: input.repository,
|
|
272
|
+
...(input.issueKey ? { issueKey: input.issueKey } : {}),
|
|
273
|
+
},
|
|
274
|
+
verification: observabilityEvidence(verdict),
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
try {
|
|
279
|
+
this.#logger.warn?.('[factory-verification] failed to report completion evidence', error);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Reporting and logging are best-effort and must not change the gate verdict.
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return verdict;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const execFileAsync = promisify(execFile);
|
|
290
|
+
export async function resolveGitHeadRevision(repositoryPath) {
|
|
291
|
+
let stdout;
|
|
292
|
+
try {
|
|
293
|
+
({ stdout } = await execFileAsync('git', ['rev-parse', '--verify', 'HEAD'], {
|
|
294
|
+
cwd: repositoryPath,
|
|
295
|
+
encoding: 'utf8',
|
|
296
|
+
maxBuffer: 64 * 1024,
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
throw new Error(`could not resolve feature checkout HEAD: ${message(error)}`);
|
|
301
|
+
}
|
|
302
|
+
const revision = stdout.trim();
|
|
303
|
+
if (!/^[a-f0-9]{40,64}$/u.test(revision)) {
|
|
304
|
+
throw new Error('feature checkout HEAD was not a valid Git object id');
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
({ stdout } = await execFileAsync('git', ['status', '--porcelain=v1', '--untracked-files=all'], {
|
|
308
|
+
cwd: repositoryPath,
|
|
309
|
+
encoding: 'utf8',
|
|
310
|
+
maxBuffer: 1024 * 1024,
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
throw new Error(`could not inspect feature checkout state: ${message(error)}`);
|
|
315
|
+
}
|
|
316
|
+
if (stdout.trim()) {
|
|
317
|
+
throw new Error('feature checkout contains uncommitted changes');
|
|
318
|
+
}
|
|
319
|
+
return revision;
|
|
320
|
+
}
|
|
321
|
+
export async function runE2eCommand(input) {
|
|
322
|
+
const started = Date.now();
|
|
323
|
+
input.signal.throwIfAborted();
|
|
324
|
+
const runner = new ProcessCommandRunner();
|
|
325
|
+
const connection = { context: input.environment.kubeContext };
|
|
326
|
+
const kubectl = kubectlConnectionArgs(connection);
|
|
327
|
+
const pod = dnsLabel(`factory-e2e-${input.env.FACTORY_VERIFICATION_RUN_ID ?? randomUUID()}`)
|
|
328
|
+
.slice(0, 63);
|
|
329
|
+
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'factory-verification-e2e-'));
|
|
330
|
+
const archivePath = join(temporaryDirectory, 'source.tar');
|
|
331
|
+
const environmentEntries = Object.entries(input.env)
|
|
332
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
333
|
+
.map(([key, value]) => {
|
|
334
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) {
|
|
335
|
+
throw new Error(`invalid E2E environment variable name ${JSON.stringify(key)}`);
|
|
336
|
+
}
|
|
337
|
+
return `${key}=${value}`;
|
|
338
|
+
});
|
|
339
|
+
const podResource = {
|
|
340
|
+
apiVersion: 'v1',
|
|
341
|
+
kind: 'Pod',
|
|
342
|
+
metadata: {
|
|
343
|
+
name: pod,
|
|
344
|
+
namespace: input.environment.namespace,
|
|
345
|
+
labels: {
|
|
346
|
+
'app.kubernetes.io/managed-by': 'factory',
|
|
347
|
+
'factory.agentworkforce.dev/environment-id': input.environment.id,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
spec: {
|
|
351
|
+
restartPolicy: 'Never',
|
|
352
|
+
terminationGracePeriodSeconds: 1,
|
|
353
|
+
activeDeadlineSeconds: Math.max(60, Math.ceil(input.timeoutMs / 1_000) + 120),
|
|
354
|
+
serviceAccountName: 'factory-guardrail-workload',
|
|
355
|
+
automountServiceAccountToken: false,
|
|
356
|
+
securityContext: {
|
|
357
|
+
runAsNonRoot: true,
|
|
358
|
+
runAsUser: 1_000,
|
|
359
|
+
runAsGroup: 1_000,
|
|
360
|
+
fsGroup: 1_000,
|
|
361
|
+
seccompProfile: { type: 'RuntimeDefault' },
|
|
362
|
+
},
|
|
363
|
+
containers: [{
|
|
364
|
+
name: 'runner',
|
|
365
|
+
image: input.image,
|
|
366
|
+
imagePullPolicy: 'IfNotPresent',
|
|
367
|
+
command: ['sh', '-c', 'while true; do sleep 3600; done'],
|
|
368
|
+
securityContext: {
|
|
369
|
+
allowPrivilegeEscalation: false,
|
|
370
|
+
readOnlyRootFilesystem: true,
|
|
371
|
+
capabilities: { drop: ['ALL'] },
|
|
372
|
+
},
|
|
373
|
+
resources: {
|
|
374
|
+
requests: { cpu: '25m', memory: '64Mi' },
|
|
375
|
+
limits: { cpu: '1', memory: '1Gi' },
|
|
376
|
+
},
|
|
377
|
+
volumeMounts: [
|
|
378
|
+
{ name: 'workspace', mountPath: '/workspace' },
|
|
379
|
+
{ name: 'tmp', mountPath: '/tmp' },
|
|
380
|
+
],
|
|
381
|
+
}],
|
|
382
|
+
volumes: [
|
|
383
|
+
{ name: 'workspace', emptyDir: {} },
|
|
384
|
+
{ name: 'tmp', emptyDir: {} },
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
try {
|
|
389
|
+
await execFileAsync('git', [
|
|
390
|
+
'-C', input.cwd,
|
|
391
|
+
'archive', '--format=tar', `--output=${archivePath}`, 'HEAD',
|
|
392
|
+
], { encoding: 'utf8', maxBuffer: 1024 * 1024 });
|
|
393
|
+
await runner.run('kubectl', [
|
|
394
|
+
...kubectl,
|
|
395
|
+
'--namespace', input.environment.namespace,
|
|
396
|
+
'create', '--filename', '-',
|
|
397
|
+
], {
|
|
398
|
+
input: JSON.stringify(podResource),
|
|
399
|
+
timeoutMs: 30_000,
|
|
400
|
+
signal: input.signal,
|
|
401
|
+
});
|
|
402
|
+
await runner.run('kubectl', [
|
|
403
|
+
...kubectl,
|
|
404
|
+
'--namespace', input.environment.namespace,
|
|
405
|
+
'wait', `pod/${pod}`, '--for=condition=Ready', '--timeout=120s',
|
|
406
|
+
], { timeoutMs: 125_000, signal: input.signal });
|
|
407
|
+
await runner.run('kubectl', [
|
|
408
|
+
...kubectl,
|
|
409
|
+
'--namespace', input.environment.namespace,
|
|
410
|
+
'cp', archivePath, `${pod}:/tmp/source.tar`,
|
|
411
|
+
], { timeoutMs: 120_000, signal: input.signal });
|
|
412
|
+
await runner.run('kubectl', [
|
|
413
|
+
...kubectl,
|
|
414
|
+
'--namespace', input.environment.namespace,
|
|
415
|
+
'exec', pod, '--',
|
|
416
|
+
'tar', '-xf', '/tmp/source.tar', '-C', '/workspace',
|
|
417
|
+
], { timeoutMs: 120_000, signal: input.signal });
|
|
418
|
+
try {
|
|
419
|
+
const result = await runner.run('kubectl', [
|
|
420
|
+
...kubectl,
|
|
421
|
+
'--namespace', input.environment.namespace,
|
|
422
|
+
'exec', pod, '--',
|
|
423
|
+
'env', ...environmentEntries,
|
|
424
|
+
'sh', '-c', 'cd /workspace && exec "$@"',
|
|
425
|
+
'factory-e2e', input.command, ...input.args,
|
|
426
|
+
], { timeoutMs: input.timeoutMs, signal: input.signal });
|
|
427
|
+
return { exitCode: 0, ...result, durationMs: elapsed(started) };
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
if (input.signal.aborted || /timed out after/iu.test(message(error))) {
|
|
431
|
+
throw new VerificationTimeoutError(`E2E stage timed out after ${input.timeoutMs}ms`);
|
|
432
|
+
}
|
|
433
|
+
if (error instanceof CommandExecutionError) {
|
|
434
|
+
return {
|
|
435
|
+
exitCode: 1,
|
|
436
|
+
stdout: error.stdout,
|
|
437
|
+
stderr: error.stderr || error.message,
|
|
438
|
+
durationMs: elapsed(started),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
await runner.run('kubectl', [
|
|
446
|
+
...kubectl,
|
|
447
|
+
'--namespace', input.environment.namespace,
|
|
448
|
+
'delete', 'pod', pod,
|
|
449
|
+
'--ignore-not-found=true', '--wait=true', '--grace-period=0', '--force',
|
|
450
|
+
], { timeoutMs: 60_000 }).catch(() => undefined);
|
|
451
|
+
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const defaultLoadRunner = async (environment, stack, runId, signal) => {
|
|
455
|
+
const profile = await loadLoadProfile(stack.load.profilePath);
|
|
456
|
+
const result = await runLoad({
|
|
457
|
+
id: environment.id,
|
|
458
|
+
namespace: environment.namespace,
|
|
459
|
+
kubeContext: environment.kubeContext,
|
|
460
|
+
endpoints: environment.internalEndpoints,
|
|
461
|
+
}, profile, {
|
|
462
|
+
namespace: environment.namespace,
|
|
463
|
+
kubeContext: environment.kubeContext,
|
|
464
|
+
timeoutMs: stack.load.timeoutMs,
|
|
465
|
+
k6Image: stack.load.k6Image,
|
|
466
|
+
runId,
|
|
467
|
+
signal,
|
|
468
|
+
});
|
|
469
|
+
return { status: result.status, measured: result.measured, violations: result.violations };
|
|
470
|
+
};
|
|
471
|
+
function endpointEnvironment(environment) {
|
|
472
|
+
const values = {};
|
|
473
|
+
for (const [name, endpoint] of Object.entries(environment.endpoints)) {
|
|
474
|
+
const key = name.toUpperCase().replace(/[^A-Z0-9]+/gu, '_').replace(/^_+|_+$/gu, '');
|
|
475
|
+
values[`FACTORY_EXTERNAL_ENDPOINT_${key}`] = endpoint;
|
|
476
|
+
}
|
|
477
|
+
for (const [name, endpoint] of Object.entries(environment.internalEndpoints)) {
|
|
478
|
+
const key = name.toUpperCase().replace(/[^A-Z0-9]+/gu, '_').replace(/^_+|_+$/gu, '');
|
|
479
|
+
values[`FACTORY_ENDPOINT_${key}`] = endpoint;
|
|
480
|
+
values[`FACTORY_INTERNAL_ENDPOINT_${key}`] = endpoint;
|
|
481
|
+
}
|
|
482
|
+
return values;
|
|
483
|
+
}
|
|
484
|
+
function observabilityEvidence(verdict) {
|
|
485
|
+
const { evidence } = verdict;
|
|
486
|
+
return {
|
|
487
|
+
contract: evidence.contract,
|
|
488
|
+
environmentId: evidence.environmentId,
|
|
489
|
+
namespace: evidence.namespace,
|
|
490
|
+
verdict: verdict.status,
|
|
491
|
+
timedOut: evidence.timedOut,
|
|
492
|
+
stages: {
|
|
493
|
+
resolve: evidence.stages.resolve.status,
|
|
494
|
+
provision: evidence.stages.provision.status,
|
|
495
|
+
deploy: evidence.stages.deploy.status,
|
|
496
|
+
e2e: evidence.stages.e2e.status,
|
|
497
|
+
load: evidence.stages.load.status,
|
|
498
|
+
evaluate: evidence.stages.evaluate.status,
|
|
499
|
+
teardown: evidence.stages.teardown.status,
|
|
500
|
+
},
|
|
501
|
+
e2e: {
|
|
502
|
+
durationMs: evidence.stages.e2e.durationMs,
|
|
503
|
+
exitCode: evidence.stages.e2e.exitCode,
|
|
504
|
+
},
|
|
505
|
+
load: evidence.stages.load.measured ? {
|
|
506
|
+
durationMs: evidence.stages.load.durationMs,
|
|
507
|
+
measured: evidence.stages.load.measured,
|
|
508
|
+
violations: evidence.stages.load.violations ?? [],
|
|
509
|
+
} : undefined,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
class Semaphore {
|
|
513
|
+
#capacity;
|
|
514
|
+
#active = 0;
|
|
515
|
+
#waiters = [];
|
|
516
|
+
constructor(capacity) {
|
|
517
|
+
this.#capacity = capacity;
|
|
518
|
+
}
|
|
519
|
+
async acquire(signal) {
|
|
520
|
+
if (signal.aborted)
|
|
521
|
+
throw new VerificationTimeoutError('verification aborted before environment admission');
|
|
522
|
+
if (this.#active < this.#capacity)
|
|
523
|
+
return this.#claim();
|
|
524
|
+
return await new Promise((resolve, reject) => {
|
|
525
|
+
const waiter = {
|
|
526
|
+
resolve,
|
|
527
|
+
reject,
|
|
528
|
+
signal,
|
|
529
|
+
abort: () => {
|
|
530
|
+
const index = this.#waiters.indexOf(waiter);
|
|
531
|
+
if (index >= 0)
|
|
532
|
+
this.#waiters.splice(index, 1);
|
|
533
|
+
reject(new VerificationTimeoutError('verification aborted waiting for environment admission'));
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
this.#waiters.push(waiter);
|
|
537
|
+
signal.addEventListener('abort', waiter.abort, { once: true });
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
#claim() {
|
|
541
|
+
this.#active += 1;
|
|
542
|
+
let released = false;
|
|
543
|
+
return () => {
|
|
544
|
+
if (released)
|
|
545
|
+
return;
|
|
546
|
+
released = true;
|
|
547
|
+
this.#active -= 1;
|
|
548
|
+
this.#drain();
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
#drain() {
|
|
552
|
+
while (this.#active < this.#capacity && this.#waiters.length > 0) {
|
|
553
|
+
const waiter = this.#waiters.shift();
|
|
554
|
+
waiter.signal.removeEventListener('abort', waiter.abort);
|
|
555
|
+
if (waiter.signal.aborted) {
|
|
556
|
+
waiter.reject(new VerificationTimeoutError('verification aborted waiting for environment admission'));
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
waiter.resolve(this.#claim());
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
export class VerificationTimeoutError extends Error {
|
|
564
|
+
constructor(message) {
|
|
565
|
+
super(message);
|
|
566
|
+
this.name = 'VerificationTimeoutError';
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async function withinTimeout(operation, timeoutMs, label, signal) {
|
|
570
|
+
if (signal?.aborted)
|
|
571
|
+
throw new VerificationTimeoutError(`${label} aborted`);
|
|
572
|
+
let timer;
|
|
573
|
+
let abort;
|
|
574
|
+
const timeout = new Promise((_, reject) => {
|
|
575
|
+
timer = setTimeout(() => reject(new VerificationTimeoutError(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
576
|
+
if (signal) {
|
|
577
|
+
abort = () => reject(new VerificationTimeoutError(`${label} aborted`));
|
|
578
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
try {
|
|
582
|
+
return await Promise.race([operation, timeout]);
|
|
583
|
+
}
|
|
584
|
+
finally {
|
|
585
|
+
if (timer)
|
|
586
|
+
clearTimeout(timer);
|
|
587
|
+
if (abort)
|
|
588
|
+
signal?.removeEventListener('abort', abort);
|
|
589
|
+
void operation.catch(() => undefined);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const emptyStage = () => ({ status: 'skipped', durationMs: 0 });
|
|
593
|
+
const emptyStages = () => ({
|
|
594
|
+
resolve: emptyStage(),
|
|
595
|
+
provision: emptyStage(),
|
|
596
|
+
deploy: emptyStage(),
|
|
597
|
+
e2e: emptyStage(),
|
|
598
|
+
load: emptyStage(),
|
|
599
|
+
evaluate: emptyStage(),
|
|
600
|
+
teardown: emptyStage(),
|
|
601
|
+
});
|
|
602
|
+
const passStage = (started) => ({ status: 'pass', durationMs: elapsed(started) });
|
|
603
|
+
const failStage = (started, error) => ({
|
|
604
|
+
status: 'fail',
|
|
605
|
+
durationMs: elapsed(started),
|
|
606
|
+
error: message(error),
|
|
607
|
+
});
|
|
608
|
+
const timeoutOrFailure = (started, error, signal) => ({
|
|
609
|
+
status: signal?.aborted || error instanceof VerificationTimeoutError ? 'timed_out' : 'fail',
|
|
610
|
+
durationMs: elapsed(started),
|
|
611
|
+
error: message(error),
|
|
612
|
+
});
|
|
613
|
+
const stageReason = (stage, error, signal) => signal?.aborted || error instanceof VerificationTimeoutError
|
|
614
|
+
? `${stage} stage timed out: ${message(error)}`
|
|
615
|
+
: `${stage} stage failed: ${message(error)}`;
|
|
616
|
+
const elapsed = (started) => Math.max(0, Date.now() - started);
|
|
617
|
+
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
618
|
+
const dnsLabel = (value) => value
|
|
619
|
+
.toLowerCase()
|
|
620
|
+
.replace(/[^a-z0-9-]+/gu, '-')
|
|
621
|
+
.replace(/^-+|-+$/gu, '') || 'verification';
|
|
622
|
+
const positiveInteger = (value, field) => {
|
|
623
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
624
|
+
throw new Error(`${field} must be a positive integer`);
|
|
625
|
+
return value;
|
|
626
|
+
};
|
|
627
|
+
function asVerificationEnvironment(environment, stack, endpoints) {
|
|
628
|
+
const namespace = environment.namespace;
|
|
629
|
+
const internalEndpoints = Object.fromEntries(stack.loaded.descriptor.endpoints.map((endpoint) => [
|
|
630
|
+
endpoint.name,
|
|
631
|
+
`${endpoint.protocol}://${endpoint.service}.${namespace}.svc.cluster.local:${endpoint.port}${endpoint.path}`,
|
|
632
|
+
]));
|
|
633
|
+
return {
|
|
634
|
+
id: environment.id,
|
|
635
|
+
namespace,
|
|
636
|
+
endpoints,
|
|
637
|
+
internalEndpoints,
|
|
638
|
+
...(environment.kubeContext ? { kubeContext: environment.kubeContext } : {}),
|
|
639
|
+
expiresAt: environment.expiresAt,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
const remainingTimeout = (stack, maxRunTimeoutMs, startedAt) => Math.max(1, Math.min(stack.timeouts.overallMs, maxRunTimeoutMs) - (Date.now() - startedAt.getTime()));
|
|
643
|
+
//# sourceMappingURL=verification-pipeline.js.map
|