@monster-orange/den 0.0.0-stage → 0.1.0
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/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +166 -2
- package/bin/den +61 -0
- package/docs/distributions.md +256 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +104 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +406 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +566 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +178 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/workspace-routes.ts +2154 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Reader
|
|
3
|
+
* Reads and queries test results from JSONL files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile, readdir } from "fs/promises";
|
|
7
|
+
import { resolve, join } from "path";
|
|
8
|
+
import type {
|
|
9
|
+
TestResult,
|
|
10
|
+
TestSummary,
|
|
11
|
+
FlakyTest,
|
|
12
|
+
TrendData,
|
|
13
|
+
Pillar,
|
|
14
|
+
} from "../types/index.js";
|
|
15
|
+
|
|
16
|
+
export interface QueryFilters {
|
|
17
|
+
pillar?: Pillar;
|
|
18
|
+
environment?: string;
|
|
19
|
+
dateFrom?: Date;
|
|
20
|
+
dateTo?: Date;
|
|
21
|
+
status?: "passed" | "failed" | "skipped";
|
|
22
|
+
user?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ResultReader {
|
|
26
|
+
private resultsDir: string;
|
|
27
|
+
|
|
28
|
+
constructor(resultsDir: string = "./results") {
|
|
29
|
+
this.resultsDir = resolve(resultsDir);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Query results with filters
|
|
34
|
+
*/
|
|
35
|
+
async query(filters: QueryFilters = {}): Promise<TestResult[]> {
|
|
36
|
+
const files = await this.getRelevantFiles(filters);
|
|
37
|
+
const results: TestResult[] = [];
|
|
38
|
+
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
try {
|
|
41
|
+
const fileResults = await this.readJSONLFile(file);
|
|
42
|
+
const filtered = fileResults.filter((r) =>
|
|
43
|
+
this.matchesFilters(r, filters),
|
|
44
|
+
);
|
|
45
|
+
results.push(...filtered);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`Failed to read file ${file}: ${(error as Error).message}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Sort by timestamp (newest first)
|
|
54
|
+
return results.sort(
|
|
55
|
+
(a, b) =>
|
|
56
|
+
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get summary statistics for a pillar/environment
|
|
62
|
+
*/
|
|
63
|
+
async getSummary(
|
|
64
|
+
pillar: Pillar,
|
|
65
|
+
environment: string,
|
|
66
|
+
days: number = 7,
|
|
67
|
+
): Promise<TestSummary> {
|
|
68
|
+
const dateFrom = new Date();
|
|
69
|
+
dateFrom.setDate(dateFrom.getDate() - days);
|
|
70
|
+
|
|
71
|
+
const results = await this.query({ pillar, environment, dateFrom });
|
|
72
|
+
|
|
73
|
+
const total = results.length;
|
|
74
|
+
const passed = results.filter((r) => r.status === "passed").length;
|
|
75
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
76
|
+
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
77
|
+
const duration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
78
|
+
const passRate = total > 0 ? (passed / total) * 100 : 0;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
pillar,
|
|
82
|
+
environment,
|
|
83
|
+
total,
|
|
84
|
+
passed,
|
|
85
|
+
failed,
|
|
86
|
+
skipped,
|
|
87
|
+
duration,
|
|
88
|
+
passRate,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Find flaky tests (tests that fail inconsistently)
|
|
94
|
+
*/
|
|
95
|
+
async getFlakyTests(pillar: Pillar, days: number = 30): Promise<FlakyTest[]> {
|
|
96
|
+
const dateFrom = new Date();
|
|
97
|
+
dateFrom.setDate(dateFrom.getDate() - days);
|
|
98
|
+
|
|
99
|
+
const results = await this.query({ pillar, dateFrom });
|
|
100
|
+
|
|
101
|
+
// Group results by test name
|
|
102
|
+
const testGroups = results.reduce(
|
|
103
|
+
(acc, r) => {
|
|
104
|
+
if (!acc[r.test]) {
|
|
105
|
+
acc[r.test] = [];
|
|
106
|
+
}
|
|
107
|
+
acc[r.test].push(r);
|
|
108
|
+
return acc;
|
|
109
|
+
},
|
|
110
|
+
{} as Record<string, TestResult[]>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Find tests with both passes and failures
|
|
114
|
+
const flakyTests: FlakyTest[] = [];
|
|
115
|
+
|
|
116
|
+
for (const [testName, testResults] of Object.entries(testGroups)) {
|
|
117
|
+
const runs = testResults.length;
|
|
118
|
+
const failures = testResults.filter((r) => r.status === "failed").length;
|
|
119
|
+
|
|
120
|
+
// Consider a test flaky if it has both passes and failures
|
|
121
|
+
if (failures > 0 && failures < runs && runs >= 3) {
|
|
122
|
+
flakyTests.push({
|
|
123
|
+
test: testName,
|
|
124
|
+
runs,
|
|
125
|
+
failures,
|
|
126
|
+
flakyRate: (failures / runs) * 100,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Sort by flaky rate (most flaky first)
|
|
132
|
+
return flakyTests.sort((a, b) => b.flakyRate - a.flakyRate);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get trend data for visualization
|
|
137
|
+
*/
|
|
138
|
+
async getTrends(
|
|
139
|
+
pillar: Pillar,
|
|
140
|
+
environment: string,
|
|
141
|
+
days: number = 30,
|
|
142
|
+
): Promise<TrendData[]> {
|
|
143
|
+
const dateFrom = new Date();
|
|
144
|
+
dateFrom.setDate(dateFrom.getDate() - days);
|
|
145
|
+
|
|
146
|
+
const results = await this.query({ pillar, environment, dateFrom });
|
|
147
|
+
|
|
148
|
+
// Group results by date
|
|
149
|
+
const dateGroups = results.reduce(
|
|
150
|
+
(acc, r) => {
|
|
151
|
+
const date = r.timestamp.split("T")[0];
|
|
152
|
+
if (!acc[date]) {
|
|
153
|
+
acc[date] = [];
|
|
154
|
+
}
|
|
155
|
+
acc[date].push(r);
|
|
156
|
+
return acc;
|
|
157
|
+
},
|
|
158
|
+
{} as Record<string, TestResult[]>,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Calculate metrics for each date
|
|
162
|
+
const trends: TrendData[] = [];
|
|
163
|
+
|
|
164
|
+
for (const [date, dayResults] of Object.entries(dateGroups)) {
|
|
165
|
+
const total = dayResults.length;
|
|
166
|
+
const passed = dayResults.filter((r) => r.status === "passed").length;
|
|
167
|
+
const passRate = total > 0 ? (passed / total) * 100 : 0;
|
|
168
|
+
const avgDuration =
|
|
169
|
+
total > 0
|
|
170
|
+
? dayResults.reduce((sum, r) => sum + r.duration, 0) / total
|
|
171
|
+
: 0;
|
|
172
|
+
|
|
173
|
+
trends.push({
|
|
174
|
+
date,
|
|
175
|
+
passRate,
|
|
176
|
+
avgDuration,
|
|
177
|
+
totalTests: total,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Sort by date (oldest first)
|
|
182
|
+
return trends.sort((a, b) => a.date.localeCompare(b.date));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read JSONL file and parse results
|
|
187
|
+
*/
|
|
188
|
+
private async readJSONLFile(filepath: string): Promise<TestResult[]> {
|
|
189
|
+
const content = await readFile(filepath, "utf-8");
|
|
190
|
+
const lines = content
|
|
191
|
+
.trim()
|
|
192
|
+
.split("\n")
|
|
193
|
+
.filter((l) => l.length > 0);
|
|
194
|
+
|
|
195
|
+
return lines
|
|
196
|
+
.map((line) => {
|
|
197
|
+
try {
|
|
198
|
+
return JSON.parse(line) as TestResult;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.warn(`Failed to parse line in ${filepath}: ${line}`);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
.filter((r): r is TestResult => r !== null);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get relevant files based on filters
|
|
209
|
+
*/
|
|
210
|
+
private async getRelevantFiles(filters: QueryFilters): Promise<string[]> {
|
|
211
|
+
try {
|
|
212
|
+
const files = await readdir(this.resultsDir);
|
|
213
|
+
|
|
214
|
+
return files
|
|
215
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
216
|
+
.filter((f) => {
|
|
217
|
+
// Filter by pillar if specified
|
|
218
|
+
if (filters.pillar && !f.startsWith(filters.pillar)) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Filter by environment if specified
|
|
223
|
+
if (filters.environment && !f.includes(`-${filters.environment}-`)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return true;
|
|
228
|
+
})
|
|
229
|
+
.map((f) => join(this.resultsDir, f));
|
|
230
|
+
} catch {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if result matches filters
|
|
237
|
+
*/
|
|
238
|
+
private matchesFilters(result: TestResult, filters: QueryFilters): boolean {
|
|
239
|
+
if (filters.status && result.status !== filters.status) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (filters.user && result.user !== filters.user) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (filters.dateFrom) {
|
|
248
|
+
const resultDate = new Date(result.timestamp);
|
|
249
|
+
if (resultDate < filters.dateFrom) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (filters.dateTo) {
|
|
255
|
+
const resultDate = new Date(result.timestamp);
|
|
256
|
+
if (resultDate > filters.dateTo) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Export singleton instance
|
|
266
|
+
export const resultReader = new ResultReader();
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Writer
|
|
3
|
+
* Writes test results to JSONL (JSON Lines) format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
appendFile,
|
|
8
|
+
writeFile,
|
|
9
|
+
readFile,
|
|
10
|
+
mkdir,
|
|
11
|
+
rename,
|
|
12
|
+
unlink,
|
|
13
|
+
} from "fs/promises";
|
|
14
|
+
import { resolve, join } from "path";
|
|
15
|
+
import type { TestResult, Pillar } from "../types/index.js";
|
|
16
|
+
|
|
17
|
+
export class ResultWriter {
|
|
18
|
+
private resultsDir: string;
|
|
19
|
+
|
|
20
|
+
constructor(resultsDir: string = "./results") {
|
|
21
|
+
this.resultsDir = resolve(resultsDir);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Write a single test result to JSONL file
|
|
26
|
+
*/
|
|
27
|
+
async writeResult(result: TestResult): Promise<void> {
|
|
28
|
+
const filename = this.getFilename(result.pillar, result.environment);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Ensure results directory exists
|
|
32
|
+
await mkdir(this.resultsDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
// Append result as JSON line
|
|
35
|
+
const line = JSON.stringify(result) + "\n";
|
|
36
|
+
await appendFile(filename, line);
|
|
37
|
+
|
|
38
|
+
// Update index
|
|
39
|
+
await this.updateIndex(result.pillar, result.environment);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Error(`Failed to write result: ${(error as Error).message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write multiple test results in batch
|
|
47
|
+
*/
|
|
48
|
+
async writeResults(results: TestResult[]): Promise<void> {
|
|
49
|
+
if (results.length === 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Group results by pillar and environment
|
|
54
|
+
const grouped = this.groupResults(results);
|
|
55
|
+
|
|
56
|
+
for (const [key, groupResults] of Object.entries(grouped)) {
|
|
57
|
+
const [pillar, environment] = key.split(":");
|
|
58
|
+
const filename = this.getFilename(pillar as Pillar, environment);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Ensure results directory exists
|
|
62
|
+
await mkdir(this.resultsDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
// Write all results as JSON lines
|
|
65
|
+
const lines =
|
|
66
|
+
groupResults.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
67
|
+
await appendFile(filename, lines);
|
|
68
|
+
|
|
69
|
+
// Update index
|
|
70
|
+
await this.updateIndex(pillar as Pillar, environment);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Failed to write results for ${pillar}/${environment}: ${(error as Error).message}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Update results index for quick lookups
|
|
81
|
+
*/
|
|
82
|
+
private async updateIndex(
|
|
83
|
+
pillar: Pillar,
|
|
84
|
+
environment: string,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const indexPath = join(this.resultsDir, "index.json");
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
let index: Record<string, unknown> = {};
|
|
90
|
+
|
|
91
|
+
// Load existing index. Distinguish "no index yet" (ENOENT → start fresh)
|
|
92
|
+
// from a corrupt/unreadable existing file — clobbering a corrupt index
|
|
93
|
+
// with {} would silently wipe every other pillar/env entry.
|
|
94
|
+
let content: string | undefined;
|
|
95
|
+
try {
|
|
96
|
+
content = await readFile(indexPath, "utf-8");
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
|
|
99
|
+
}
|
|
100
|
+
if (content !== undefined) {
|
|
101
|
+
const parsed = JSON.parse(content) as unknown;
|
|
102
|
+
if (
|
|
103
|
+
parsed === null ||
|
|
104
|
+
typeof parsed !== "object" ||
|
|
105
|
+
Array.isArray(parsed)
|
|
106
|
+
) {
|
|
107
|
+
throw new Error("existing index.json is not an object");
|
|
108
|
+
}
|
|
109
|
+
index = parsed as Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const key = `${pillar}:${environment}`;
|
|
113
|
+
const date = new Date().toISOString().split("T")[0];
|
|
114
|
+
|
|
115
|
+
// Update index entry
|
|
116
|
+
index[key] = {
|
|
117
|
+
pillar,
|
|
118
|
+
environment,
|
|
119
|
+
lastUpdated: new Date().toISOString(),
|
|
120
|
+
latestFile: this.getFilename(pillar, environment),
|
|
121
|
+
date,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Write atomically (temp + rename) so a crash/concurrent worker can't
|
|
125
|
+
// leave a half-written index.json.
|
|
126
|
+
const tmp = `${indexPath}.${process.pid}.tmp`;
|
|
127
|
+
try {
|
|
128
|
+
await writeFile(tmp, JSON.stringify(index, null, 2));
|
|
129
|
+
await rename(tmp, indexPath);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// Don't leave an orphaned temp file behind when the write/rename fails.
|
|
132
|
+
await unlink(tmp).catch(() => {});
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Index update is non-critical, just log error
|
|
137
|
+
console.warn(`Failed to update index: ${(error as Error).message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get filename for results
|
|
143
|
+
*/
|
|
144
|
+
private getFilename(pillar: string, environment: string): string {
|
|
145
|
+
const date = new Date().toISOString().split("T")[0];
|
|
146
|
+
return join(this.resultsDir, `${pillar}-${environment}-${date}.jsonl`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Group results by pillar and environment
|
|
151
|
+
*/
|
|
152
|
+
private groupResults(results: TestResult[]): Record<string, TestResult[]> {
|
|
153
|
+
return results.reduce(
|
|
154
|
+
(acc, result) => {
|
|
155
|
+
const key = `${result.pillar}:${result.environment}`;
|
|
156
|
+
if (!acc[key]) {
|
|
157
|
+
acc[key] = [];
|
|
158
|
+
}
|
|
159
|
+
acc[key].push(result);
|
|
160
|
+
return acc;
|
|
161
|
+
},
|
|
162
|
+
{} as Record<string, TestResult[]>,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Export singleton instance
|
|
168
|
+
export const resultWriter = new ResultWriter();
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doppler strategy — reads/writes flat keys with the `doppler` CLI.
|
|
3
|
+
* Selected with `secrets.provider: doppler` (the default).
|
|
4
|
+
*
|
|
5
|
+
* Project/config come from `secrets.doppler.{project,config}` in
|
|
6
|
+
* config/den.yml, else from the directory's `doppler setup` scope,
|
|
7
|
+
* else from DOPPLER_PROJECT / DOPPLER_CONFIG / DOPPLER_TOKEN.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile, spawn } from "child_process";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
import { flatKeyFor } from "./keys.js";
|
|
12
|
+
import type { PreflightResult, SecretRef, SecretsProvider } from "./types.js";
|
|
13
|
+
|
|
14
|
+
import { product } from "../product.js";
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
|
|
17
|
+
export interface DopplerOptions {
|
|
18
|
+
project?: string;
|
|
19
|
+
config?: string;
|
|
20
|
+
keys?: Record<string, string>;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class DopplerProvider implements SecretsProvider {
|
|
25
|
+
readonly name = "doppler" as const;
|
|
26
|
+
private readonly timeout: number;
|
|
27
|
+
|
|
28
|
+
constructor(private readonly opts: DopplerOptions = {}) {
|
|
29
|
+
this.timeout = opts.timeoutMs ?? 30_000;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `--project/--config` flags when configured explicitly. */
|
|
33
|
+
scopeArgs(): string[] {
|
|
34
|
+
const a: string[] = [];
|
|
35
|
+
if (this.opts.project) a.push("--project", this.opts.project);
|
|
36
|
+
if (this.opts.config) a.push("--config", this.opts.config);
|
|
37
|
+
return a;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
keyFor(ref: SecretRef): string {
|
|
41
|
+
return flatKeyFor(ref, this.opts.keys);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async isReady(): Promise<boolean> {
|
|
45
|
+
try {
|
|
46
|
+
await execFileAsync("doppler", ["me", "--json"], {
|
|
47
|
+
timeout: this.timeout,
|
|
48
|
+
});
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async get(ref: SecretRef): Promise<string> {
|
|
56
|
+
const key = this.keyFor(ref);
|
|
57
|
+
const { stdout } = await execFileAsync(
|
|
58
|
+
"doppler",
|
|
59
|
+
["secrets", "get", key, "--plain", ...this.scopeArgs()],
|
|
60
|
+
{ timeout: this.timeout },
|
|
61
|
+
);
|
|
62
|
+
return String(stdout).trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async tryGet(ref: SecretRef): Promise<string | null> {
|
|
66
|
+
try {
|
|
67
|
+
const v = await this.get(ref);
|
|
68
|
+
return v.length > 0 ? v : null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Value goes over stdin so it never appears in the process table. */
|
|
75
|
+
async set(ref: SecretRef, value: string): Promise<void> {
|
|
76
|
+
const key = this.keyFor(ref);
|
|
77
|
+
await new Promise<void>((resolve, reject) => {
|
|
78
|
+
const child = spawn(
|
|
79
|
+
"doppler",
|
|
80
|
+
["secrets", "set", key, "--silent", ...this.scopeArgs()],
|
|
81
|
+
{ stdio: ["pipe", "ignore", "pipe"] },
|
|
82
|
+
);
|
|
83
|
+
let stderr = "";
|
|
84
|
+
child.stderr?.on("data", (d) => (stderr += String(d)));
|
|
85
|
+
child.on("error", reject);
|
|
86
|
+
child.on("close", (code) =>
|
|
87
|
+
code === 0
|
|
88
|
+
? resolve()
|
|
89
|
+
: reject(
|
|
90
|
+
new Error(`doppler secrets set ${key} failed: ${stderr.trim()}`),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
child.stdin?.end(value);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async preflight(): Promise<PreflightResult> {
|
|
98
|
+
if (!(await this.isReady())) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
message:
|
|
102
|
+
"Doppler CLI is not installed or not logged in.\n" +
|
|
103
|
+
"Install: brew install dopplerhq/cli/doppler then: doppler login",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await execFileAsync(
|
|
108
|
+
"doppler",
|
|
109
|
+
["secrets", "--only-names", "--json", ...this.scopeArgs()],
|
|
110
|
+
{ timeout: this.timeout },
|
|
111
|
+
);
|
|
112
|
+
return { ok: true };
|
|
113
|
+
} catch {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
message:
|
|
117
|
+
"Doppler is logged in but no project/config is selected.\n" +
|
|
118
|
+
`Run \`doppler setup\` in the repo, or set secrets.doppler.project/config in ${product().configFile}.`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
runPrefix(): string[] {
|
|
124
|
+
return ["doppler", "run", ...this.scopeArgs(), "--"];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-variable strategy — reads flat keys from process.env.
|
|
3
|
+
* Selected with `secrets.provider: env`. Intended for CI and for engineers who
|
|
4
|
+
* inject secrets themselves (direnv, `doppler run -- den …`, CI secrets).
|
|
5
|
+
*/
|
|
6
|
+
import { flatKeyFor } from "./keys.js";
|
|
7
|
+
import type { PreflightResult, SecretRef, SecretsProvider } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export class EnvProvider implements SecretsProvider {
|
|
10
|
+
readonly name = "env" as const;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly keys: Record<string, string> = {},
|
|
14
|
+
private readonly env: NodeJS.ProcessEnv = process.env,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
keyFor(ref: SecretRef): string {
|
|
18
|
+
return flatKeyFor(ref, this.keys);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async isReady(): Promise<boolean> {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async get(ref: SecretRef): Promise<string> {
|
|
26
|
+
const key = this.keyFor(ref);
|
|
27
|
+
const v = this.env[key];
|
|
28
|
+
if (!v) throw new Error(`Secret ${key} is not set in the environment`);
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async tryGet(ref: SecretRef): Promise<string | null> {
|
|
33
|
+
return this.env[this.keyFor(ref)] || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async set(ref: SecretRef, value: string): Promise<void> {
|
|
37
|
+
// Process-local only: the env backend has no durable store.
|
|
38
|
+
this.env[this.keyFor(ref)] = value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async preflight(): Promise<PreflightResult> {
|
|
42
|
+
return { ok: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
runPrefix(): string[] {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|