@monster-orange/den 0.0.0 → 0.2.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 +204 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- package/bin/den +68 -0
- package/docs/distributions.md +499 -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 +101 -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 +403 -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 +124 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +757 -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/agent-actor.ts +77 -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 +688 -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/status.ts +74 -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 +635 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +108 -0
- package/src/shared/environment-manager/log-tail.ts +213 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +335 -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-container.ts +47 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1603 -0
- package/src/shared/environment-manager/sync-targets.ts +66 -0
- package/src/shared/environment-manager/up-plan.ts +326 -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/merge-override.ts +75 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/placement.ts +321 -0
- package/src/shared/manifest/read-manifest.ts +278 -0
- package/src/shared/manifest/schema.ts +313 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-layers.ts +229 -0
- package/src/shared/manifest/validate-manifest.ts +200 -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 +190 -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/redact.ts +107 -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 +118 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/status.ts +99 -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 +701 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +148 -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 +81 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +570 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3742 -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/mcp-main.ts +13 -0
- package/src/ui/cli/mcp-stdout-guard.ts +10 -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 +117 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1319 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/mcp/api.ts +284 -0
- package/src/ui/mcp/guards.ts +141 -0
- package/src/ui/mcp/server.ts +147 -0
- package/src/ui/mcp/tools.ts +565 -0
- package/src/ui/server/audit.ts +71 -0
- package/src/ui/server/changelog-parser.ts +105 -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/graph-prefs.ts +89 -0
- package/src/ui/server/health-row.ts +15 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3350 -0
- package/src/ui/server/log-history.ts +453 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +496 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/graph-model.js +416 -0
- package/src/ui/server/public/graph.css +976 -0
- package/src/ui/server/public/graph.js +986 -0
- package/src/ui/server/public/header.js +1373 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +5344 -0
- package/src/ui/server/public/logs.html +1496 -0
- package/src/ui/server/public/panel.js +787 -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 +1103 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/readonly-routes.ts +200 -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/ui-assets.ts +73 -0
- package/src/ui/server/workspace-routes.ts +2185 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
export interface StageSmokeTreeResult {
|
|
5
|
+
copiedFiles: number;
|
|
6
|
+
matchedTestFiles: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function testMatchToRegex(testMatch: string): RegExp {
|
|
10
|
+
const matchExt = testMatch.replace(/^\*\*\/\*\./, "").replace(/\./g, "\\.");
|
|
11
|
+
return new RegExp(`\\.${matchExt}$`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function stageSmokeTree(
|
|
15
|
+
sourceDir: string,
|
|
16
|
+
destinationDir: string,
|
|
17
|
+
testMatch: string,
|
|
18
|
+
): StageSmokeTreeResult {
|
|
19
|
+
const testRegex = testMatchToRegex(testMatch);
|
|
20
|
+
let copiedFiles = 0;
|
|
21
|
+
let matchedTestFiles = 0;
|
|
22
|
+
|
|
23
|
+
function copyRecursive(src: string, dst: string): void {
|
|
24
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
25
|
+
const sourcePath = path.join(src, entry.name);
|
|
26
|
+
const destinationPath = path.join(dst, entry.name);
|
|
27
|
+
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
fs.mkdirSync(destinationPath, { recursive: true });
|
|
30
|
+
copyRecursive(sourcePath, destinationPath);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
35
|
+
copiedFiles += 1;
|
|
36
|
+
if (testRegex.test(entry.name)) {
|
|
37
|
+
matchedTestFiles += 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
43
|
+
copyRecursive(sourceDir, destinationDir);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
copiedFiles,
|
|
47
|
+
matchedTestFiles,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `den suites`: run the test suites each repo declares in its manifest.
|
|
3
|
+
*
|
|
4
|
+
* tsx src/pillars/suites/run.ts --env local [--suite repo/name ...]
|
|
5
|
+
*
|
|
6
|
+
* A suite is a shell command run from the repo's checkout (or `cwd` under
|
|
7
|
+
* it). It gets DEN_ENV and BASE_URL (the repo entrypoint's URL for the env).
|
|
8
|
+
* Local only unless the suite lists other environments; prod never.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from "child_process";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
import { openUrlFor } from "../../shared/manifest/service-links.js";
|
|
14
|
+
import { listServicesWithManifests } from "../../shared/manifest/workspace-manifests.js";
|
|
15
|
+
import type { Environment, TestSuiteConfig } from "../../shared/types/index.js";
|
|
16
|
+
|
|
17
|
+
import { product } from "../../shared/product.js";
|
|
18
|
+
export interface PlannedSuite {
|
|
19
|
+
id: string;
|
|
20
|
+
repo: string;
|
|
21
|
+
name: string;
|
|
22
|
+
label: string;
|
|
23
|
+
cmd: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
baseUrl: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function suiteRefusal(
|
|
29
|
+
suite: TestSuiteConfig,
|
|
30
|
+
env: Environment,
|
|
31
|
+
): string | null {
|
|
32
|
+
if (env === "prod" || env === "production")
|
|
33
|
+
return `${product().title} never runs suites against production`;
|
|
34
|
+
const allowed = suite.environments ?? ["local"];
|
|
35
|
+
return allowed.includes(env)
|
|
36
|
+
? null
|
|
37
|
+
: `declared for ${allowed.join(", ")} only`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Every repo suite runnable on env; `only` narrows to repo/name or repo. */
|
|
41
|
+
export function planSuites(
|
|
42
|
+
services: ReturnType<typeof listServicesWithManifests>,
|
|
43
|
+
env: Environment,
|
|
44
|
+
only: string[] = [],
|
|
45
|
+
): { run: PlannedSuite[]; skipped: Array<{ id: string; why: string }> } {
|
|
46
|
+
const run: PlannedSuite[] = [];
|
|
47
|
+
const skipped: Array<{ id: string; why: string }> = [];
|
|
48
|
+
const seen = new Set<string>();
|
|
49
|
+
for (const s of services) {
|
|
50
|
+
if (seen.has(s.repo)) continue;
|
|
51
|
+
seen.add(s.repo);
|
|
52
|
+
const entry = s.manifest.entrypoint
|
|
53
|
+
? s.manifest.services?.[s.manifest.entrypoint]
|
|
54
|
+
: undefined;
|
|
55
|
+
for (const [name, suite] of Object.entries(s.manifest.tests ?? {})) {
|
|
56
|
+
const id = `${s.repo}/${name}`;
|
|
57
|
+
if (only.length && !only.includes(id) && !only.includes(s.repo)) continue;
|
|
58
|
+
const why = suiteRefusal(suite, env);
|
|
59
|
+
if (why) {
|
|
60
|
+
skipped.push({ id, why });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const cwd = path.resolve(s.repoPath, suite.cwd ?? ".");
|
|
64
|
+
const root = path.resolve(s.repoPath);
|
|
65
|
+
if (cwd !== root && !cwd.startsWith(root + path.sep)) {
|
|
66
|
+
skipped.push({ id, why: "cwd leaves the checkout" });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
run.push({
|
|
70
|
+
id,
|
|
71
|
+
repo: s.repo,
|
|
72
|
+
name,
|
|
73
|
+
label: suite.label ?? name,
|
|
74
|
+
cmd: suite.cmd,
|
|
75
|
+
cwd,
|
|
76
|
+
baseUrl: openUrlFor(entry, env),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { run, skipped };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runSuite(s: PlannedSuite, env: Environment): Promise<number> {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const child = spawn("sh", ["-c", s.cmd], {
|
|
86
|
+
cwd: s.cwd,
|
|
87
|
+
stdio: "inherit",
|
|
88
|
+
env: {
|
|
89
|
+
...process.env,
|
|
90
|
+
DEN_ENV: env,
|
|
91
|
+
...(s.baseUrl ? { BASE_URL: s.baseUrl } : {}),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
child.on("error", () => resolve(127));
|
|
95
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
100
|
+
const i = argv.indexOf("--env");
|
|
101
|
+
const env = (
|
|
102
|
+
i >= 0 && argv[i + 1]
|
|
103
|
+
? argv[i + 1]
|
|
104
|
+
: (argv.find((a) => a.startsWith("--env="))?.slice(6) ?? "local")
|
|
105
|
+
) as Environment;
|
|
106
|
+
const only: string[] = [];
|
|
107
|
+
argv.forEach((a, i) => {
|
|
108
|
+
if (a === "--suite" && argv[i + 1]) only.push(argv[i + 1]);
|
|
109
|
+
else if (a.startsWith("--suite=")) only.push(a.slice(8));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const { run, skipped } = planSuites(listServicesWithManifests(), env, only);
|
|
113
|
+
for (const s of skipped) console.log(`– ${s.id}: skipped (${s.why})`);
|
|
114
|
+
if (run.length === 0) {
|
|
115
|
+
console.log(
|
|
116
|
+
`No repo suite to run on ${env}. Declare them under tests: in ${product().manifestYml}.`,
|
|
117
|
+
);
|
|
118
|
+
return skipped.length && only.length ? 2 : 0;
|
|
119
|
+
}
|
|
120
|
+
const results: Array<{ id: string; code: number; ms: number }> = [];
|
|
121
|
+
for (const s of run) {
|
|
122
|
+
console.log(`\n▶ ${s.id}: ${s.cmd} (in ${s.cwd})`);
|
|
123
|
+
const t0 = Date.now();
|
|
124
|
+
const code = await runSuite(s, env);
|
|
125
|
+
results.push({ id: s.id, code, ms: Date.now() - t0 });
|
|
126
|
+
console.log(`${code === 0 ? "✓" : "✗"} ${s.id} exited ${code}`);
|
|
127
|
+
}
|
|
128
|
+
console.log("\nSuites:");
|
|
129
|
+
for (const r of results)
|
|
130
|
+
console.log(
|
|
131
|
+
` ${r.code === 0 ? "✓" : "✗"} ${r.id} ${(r.ms / 1000).toFixed(1)}s`,
|
|
132
|
+
);
|
|
133
|
+
return results.some((r) => r.code !== 0) ? 1 : 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const isDirectRun =
|
|
137
|
+
process.argv[1] &&
|
|
138
|
+
path.resolve(process.argv[1]).includes("pillars/suites/run");
|
|
139
|
+
if (isDirectRun) {
|
|
140
|
+
main().then(
|
|
141
|
+
(code) => process.exit(code),
|
|
142
|
+
(err) => {
|
|
143
|
+
console.error(`✗ ${(err as Error).message}`);
|
|
144
|
+
process.exit(2);
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* configure-aws-profiles
|
|
3
|
+
*
|
|
4
|
+
* Interactively maps den environments to AWS CLI profiles.
|
|
5
|
+
* Writes the selection to config/local/aws-profiles.json (gitignored).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* make aws-configure
|
|
9
|
+
* npx tsx src/scripts/configure-aws-profiles.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { execSync } from "child_process";
|
|
15
|
+
import inquirer from "inquirer";
|
|
16
|
+
import type { Environment } from "../shared/types/index.js";
|
|
17
|
+
import { getDistRoot } from "../shared/paths.js";
|
|
18
|
+
|
|
19
|
+
import { product } from "../shared/product.js";
|
|
20
|
+
// Den-root anchored: WRITES config/local/aws-profiles.json, and the reader
|
|
21
|
+
// (shared/environment/aws-profiles.ts) already anchors at getDistRoot().
|
|
22
|
+
const ROOT = getDistRoot();
|
|
23
|
+
const CONFIG_DIR = path.join(ROOT, "config", "local");
|
|
24
|
+
const PROFILES_FILE = path.join(CONFIG_DIR, "aws-profiles.json");
|
|
25
|
+
|
|
26
|
+
/** Environments that may need AWS credentials (local uses Docker). */
|
|
27
|
+
const AWS_ENVS: Environment[] = ["dev", "sandbox", "staging", "production"];
|
|
28
|
+
|
|
29
|
+
function listAwsProfiles(): string[] {
|
|
30
|
+
try {
|
|
31
|
+
const out = execSync("aws configure list-profiles", {
|
|
32
|
+
encoding: "utf-8",
|
|
33
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
34
|
+
});
|
|
35
|
+
return out.trim().split("\n").filter(Boolean);
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
console.log(`\n ${product().cli} · Configure AWS Profiles\n`);
|
|
43
|
+
console.log(" Maps each environment to an AWS CLI profile used for");
|
|
44
|
+
console.log(" CloudWatch log streaming, health checks, and re-auth.\n");
|
|
45
|
+
|
|
46
|
+
const profiles = listAwsProfiles();
|
|
47
|
+
|
|
48
|
+
if (!profiles.length) {
|
|
49
|
+
console.log(
|
|
50
|
+
" ⚠ No AWS profiles found. Run 'aws configure' or set up AWS SSO first.\n",
|
|
51
|
+
);
|
|
52
|
+
console.log(
|
|
53
|
+
" You can still save an empty config and re-run later with: make aws-configure\n",
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
console.log(
|
|
57
|
+
` Found ${profiles.length} profile(s): ${profiles.join(", ")}\n`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Load existing config
|
|
62
|
+
let existing: Partial<Record<Environment, string>> = {};
|
|
63
|
+
if (fs.existsSync(PROFILES_FILE)) {
|
|
64
|
+
try {
|
|
65
|
+
existing = JSON.parse(fs.readFileSync(PROFILES_FILE, "utf-8"));
|
|
66
|
+
console.log(" Current config:");
|
|
67
|
+
for (const [env, profile] of Object.entries(existing)) {
|
|
68
|
+
console.log(` ${env.padEnd(12)} → ${profile}`);
|
|
69
|
+
}
|
|
70
|
+
console.log("");
|
|
71
|
+
} catch {
|
|
72
|
+
/* start fresh */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const choices = [
|
|
77
|
+
{ name: "(none — skip this environment)", value: "" },
|
|
78
|
+
...profiles.map((p) => ({ name: p, value: p })),
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const questions = AWS_ENVS.map((env) => ({
|
|
82
|
+
type: "list" as const,
|
|
83
|
+
name: env,
|
|
84
|
+
message: `AWS profile for ${env}:`,
|
|
85
|
+
choices,
|
|
86
|
+
default: existing[env] ?? "",
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
const answers = await inquirer.prompt<Record<Environment, string>>(questions);
|
|
90
|
+
|
|
91
|
+
// Build result, dropping empty selections
|
|
92
|
+
const result: Partial<Record<Environment, string>> = {};
|
|
93
|
+
for (const env of AWS_ENVS) {
|
|
94
|
+
if (answers[env]) result[env] = answers[env];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
98
|
+
fs.writeFileSync(PROFILES_FILE, JSON.stringify(result, null, 2) + "\n");
|
|
99
|
+
|
|
100
|
+
console.log("\n Saved to config/local/aws-profiles.json");
|
|
101
|
+
if (Object.keys(result).length) {
|
|
102
|
+
for (const [env, profile] of Object.entries(result)) {
|
|
103
|
+
console.log(` ${env.padEnd(12)} → ${profile}`);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
console.log(" (no profiles configured)");
|
|
107
|
+
}
|
|
108
|
+
console.log("\n Re-run any time with: make aws-configure\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
main().catch((err) => {
|
|
112
|
+
console.error(err);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* register-repos — where test discovery gets its repos.
|
|
3
|
+
*
|
|
4
|
+
* With Local Dev enabled (the default), smoke/performance/synthetics
|
|
5
|
+
* discovery follows the ACTIVE WORKSPACE (see pillars/smoke/discover.ts):
|
|
6
|
+
* the same repos you develop against are the repos tests run against, and a
|
|
7
|
+
* fresh clone seeds every standard system repo managed out of the box.
|
|
8
|
+
* In that mode there is nothing to register — this script just reports the
|
|
9
|
+
* effective test sources.
|
|
10
|
+
*
|
|
11
|
+
* The legacy sibling-directory scan (interactive checkbox → writes
|
|
12
|
+
* config/local/registered-repos.json, gitignored) remains for hosted /
|
|
13
|
+
* no-local setups, where the workspace doesn't exist:
|
|
14
|
+
*
|
|
15
|
+
* make register-repos # report (or legacy scan when local off)
|
|
16
|
+
* make register-repos args=--legacy # force the interactive sibling scan
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import * as path from "path";
|
|
21
|
+
import inquirer from "inquirer";
|
|
22
|
+
import { parse as parseYaml } from "yaml";
|
|
23
|
+
import { getDistRoot } from "../shared/paths.js";
|
|
24
|
+
import { getReposRoot, isLocalEnabled } from "../shared/den-config.js";
|
|
25
|
+
import { getActiveWorkspace } from "../shared/environment-manager/workspaces.js";
|
|
26
|
+
import {
|
|
27
|
+
defaultWorkspaceRepos,
|
|
28
|
+
topUpDefaultWorkspace,
|
|
29
|
+
} from "../shared/environment-manager/default-workspace.js";
|
|
30
|
+
|
|
31
|
+
import { product } from "../shared/product.js";
|
|
32
|
+
// Anchored to the den root, NOT process.cwd() — this script WRITES
|
|
33
|
+
// config/local/* and a cwd-derived root once put den files in another repo.
|
|
34
|
+
const ROOT = getDistRoot();
|
|
35
|
+
const CONFIG_DIR = path.join(ROOT, "config", "local");
|
|
36
|
+
const REGISTERED_FILE = path.join(CONFIG_DIR, "registered-repos.json");
|
|
37
|
+
const MANIFEST_NAMES = [
|
|
38
|
+
`${product().manifestYml}`,
|
|
39
|
+
`${product().manifestJson}`,
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
// ── Mode selection (pure — unit tested) ───────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export type RegistrationPlan =
|
|
45
|
+
| {
|
|
46
|
+
mode: "workspace";
|
|
47
|
+
workspaceName: string;
|
|
48
|
+
repos: { name: string; managed: boolean }[];
|
|
49
|
+
}
|
|
50
|
+
| { mode: "defaults"; repos: string[] }
|
|
51
|
+
| { mode: "legacy" };
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Decide what running register-repos should do.
|
|
55
|
+
*
|
|
56
|
+
* Workspace-driven discovery makes registration a no-op, so the only reasons
|
|
57
|
+
* to run the legacy sibling scan are an explicit --legacy or Local Dev being
|
|
58
|
+
* disabled (hosted setups, where no workspace exists to follow).
|
|
59
|
+
*/
|
|
60
|
+
export function planRegistration(opts: {
|
|
61
|
+
localEnabled: boolean;
|
|
62
|
+
legacyRequested: boolean;
|
|
63
|
+
workspace: {
|
|
64
|
+
name: string;
|
|
65
|
+
services: Record<string, { mode?: string }>;
|
|
66
|
+
} | null;
|
|
67
|
+
defaultRepos: string[];
|
|
68
|
+
}): RegistrationPlan {
|
|
69
|
+
if (opts.legacyRequested || !opts.localEnabled) return { mode: "legacy" };
|
|
70
|
+
// An EMPTY workspace (declined seed, or user-emptied) reports like no
|
|
71
|
+
// workspace: the defaults message says what the first den command will
|
|
72
|
+
// do, where a zero-row workspace table says nothing actionable.
|
|
73
|
+
if (opts.workspace && Object.keys(opts.workspace.services).length > 0) {
|
|
74
|
+
return {
|
|
75
|
+
mode: "workspace",
|
|
76
|
+
workspaceName: opts.workspace.name,
|
|
77
|
+
repos: Object.entries(opts.workspace.services).map(([name, svc]) => ({
|
|
78
|
+
name,
|
|
79
|
+
// Missing mode means dev-managed everywhere else in the system
|
|
80
|
+
// (normalizeMode in workspaces.ts) — mirror that, never invert it.
|
|
81
|
+
managed: svc.mode === "managed",
|
|
82
|
+
})),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { mode: "defaults", repos: opts.defaultRepos };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Legacy sibling scan (hosted / no-local setups) ────────────────────────────
|
|
89
|
+
|
|
90
|
+
interface Candidate {
|
|
91
|
+
name: string;
|
|
92
|
+
repoPath: string;
|
|
93
|
+
description: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function scanForManifests(searchDir: string): Candidate[] {
|
|
97
|
+
const candidates: Candidate[] = [];
|
|
98
|
+
|
|
99
|
+
let entries: fs.Dirent[];
|
|
100
|
+
try {
|
|
101
|
+
entries = fs.readdirSync(searchDir, { withFileTypes: true });
|
|
102
|
+
} catch {
|
|
103
|
+
console.warn(` Could not read directory: ${searchDir}`);
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
109
|
+
|
|
110
|
+
const repoPath = path.join(searchDir, entry.name);
|
|
111
|
+
|
|
112
|
+
// Skip den itself
|
|
113
|
+
try {
|
|
114
|
+
if (fs.realpathSync(repoPath) === fs.realpathSync(ROOT)) continue;
|
|
115
|
+
} catch {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const manifestPath = MANIFEST_NAMES.map((n) => path.join(repoPath, n)).find(
|
|
120
|
+
(p) => fs.existsSync(p),
|
|
121
|
+
);
|
|
122
|
+
if (!manifestPath) continue;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
126
|
+
const manifest = (
|
|
127
|
+
manifestPath.endsWith(".yml") ? parseYaml(raw) : JSON.parse(raw)
|
|
128
|
+
) as {
|
|
129
|
+
name?: string;
|
|
130
|
+
smoke?: Record<string, unknown>;
|
|
131
|
+
services?: Record<string, unknown>;
|
|
132
|
+
synthetics?: unknown;
|
|
133
|
+
journeys?: unknown;
|
|
134
|
+
security?: Record<string, unknown>;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const capabilities: string[] = [];
|
|
138
|
+
if (manifest.smoke)
|
|
139
|
+
capabilities.push(`smoke(${Object.keys(manifest.smoke).join(",")})`);
|
|
140
|
+
if (manifest.services)
|
|
141
|
+
capabilities.push(
|
|
142
|
+
`services(${Object.keys(manifest.services).join(",")})`,
|
|
143
|
+
);
|
|
144
|
+
if (manifest.synthetics) capabilities.push("synthetics");
|
|
145
|
+
if (manifest.journeys) capabilities.push("journeys");
|
|
146
|
+
if (manifest.security)
|
|
147
|
+
capabilities.push(
|
|
148
|
+
`security(${Object.keys(manifest.security).join(",")})`,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
candidates.push({
|
|
152
|
+
name: manifest.name ?? entry.name,
|
|
153
|
+
repoPath,
|
|
154
|
+
description: capabilities.length
|
|
155
|
+
? capabilities.join(" ")
|
|
156
|
+
: "(no capabilities)",
|
|
157
|
+
});
|
|
158
|
+
} catch {
|
|
159
|
+
// Malformed manifest — still offer it
|
|
160
|
+
candidates.push({
|
|
161
|
+
name: entry.name,
|
|
162
|
+
repoPath,
|
|
163
|
+
description: "(unreadable manifest)",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return candidates;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function legacyScanAndSelect(): Promise<void> {
|
|
172
|
+
console.log(`\n ${product().cli} · Register Repos (legacy sibling scan)\n`);
|
|
173
|
+
|
|
174
|
+
// Load current registrations
|
|
175
|
+
let existing: string[] = [];
|
|
176
|
+
if (fs.existsSync(REGISTERED_FILE)) {
|
|
177
|
+
try {
|
|
178
|
+
existing = JSON.parse(
|
|
179
|
+
fs.readFileSync(REGISTERED_FILE, "utf-8"),
|
|
180
|
+
) as string[];
|
|
181
|
+
console.log(
|
|
182
|
+
` Current: ${existing.length} repo(s) registered for testing\n`,
|
|
183
|
+
);
|
|
184
|
+
} catch {
|
|
185
|
+
/* corrupt file — start fresh */
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Scan the configured repos root (defaults to the den root's parent)
|
|
190
|
+
const parentDir = getReposRoot();
|
|
191
|
+
console.log(
|
|
192
|
+
` Scanning ${parentDir} for ${product().manifestYml} / ${product().manifestJson} ...\n`,
|
|
193
|
+
);
|
|
194
|
+
const candidates = scanForManifests(parentDir);
|
|
195
|
+
|
|
196
|
+
if (!candidates.length) {
|
|
197
|
+
console.log(
|
|
198
|
+
` No sibling repos with ${product().manifestYml} or ${product().manifestJson} found.`,
|
|
199
|
+
);
|
|
200
|
+
console.log(
|
|
201
|
+
" Add custom paths manually to config/local/registered-repos.json\n",
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Prompt
|
|
207
|
+
const { selected } = await inquirer.prompt<{ selected: string[] }>([
|
|
208
|
+
{
|
|
209
|
+
type: "checkbox",
|
|
210
|
+
name: "selected",
|
|
211
|
+
message: `Select repos to register with ${product().cli} (space to toggle, enter to confirm):`,
|
|
212
|
+
choices: candidates.map((c) => ({
|
|
213
|
+
name: `${c.name.padEnd(20)} ${c.repoPath} ${c.description}`,
|
|
214
|
+
value: c.repoPath,
|
|
215
|
+
checked: existing.includes(c.repoPath),
|
|
216
|
+
})),
|
|
217
|
+
pageSize: 15,
|
|
218
|
+
},
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
// Persist
|
|
222
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
223
|
+
fs.writeFileSync(REGISTERED_FILE, JSON.stringify(selected, null, 2) + "\n");
|
|
224
|
+
|
|
225
|
+
console.log(
|
|
226
|
+
`\n Registered ${selected.length} repo(s) → config/local/registered-repos.json`,
|
|
227
|
+
);
|
|
228
|
+
console.log(" Run `make register-repos` again at any time to update.\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Entry ─────────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
async function main(): Promise<void> {
|
|
234
|
+
const legacyRequested = process.argv.includes("--legacy");
|
|
235
|
+
const localEnabled = isLocalEnabled();
|
|
236
|
+
|
|
237
|
+
// Converge first: a workspace seeded before a repo joined the blueprint
|
|
238
|
+
// (tasks-api, say) is missing a first-class system repo — add it so the
|
|
239
|
+
// report below shows the true standard set, not a stale snapshot. Skipped
|
|
240
|
+
// on the legacy / no-local paths, which never read the workspace and must
|
|
241
|
+
// not write it either.
|
|
242
|
+
const toppedUp =
|
|
243
|
+
legacyRequested || !localEnabled ? [] : topUpDefaultWorkspace();
|
|
244
|
+
if (toppedUp.length > 0) {
|
|
245
|
+
console.log(
|
|
246
|
+
`\n Added standard repo(s) to the default workspace: ${toppedUp.join(", ")} (managed)`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const active = getActiveWorkspace();
|
|
251
|
+
const plan = planRegistration({
|
|
252
|
+
localEnabled,
|
|
253
|
+
legacyRequested,
|
|
254
|
+
workspace: active
|
|
255
|
+
? { name: active.name, services: active.config.services }
|
|
256
|
+
: null,
|
|
257
|
+
defaultRepos: defaultWorkspaceRepos(),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (plan.mode === "workspace") {
|
|
261
|
+
console.log(`\n ${product().cli} · Test repo sources\n`);
|
|
262
|
+
console.log(
|
|
263
|
+
` Local Dev is enabled — test discovery follows your active workspace` +
|
|
264
|
+
` ("${plan.workspaceName}"):\n the repos you develop against are the` +
|
|
265
|
+
` repos tests run against. Nothing to register.\n`,
|
|
266
|
+
);
|
|
267
|
+
for (const r of plan.repos) {
|
|
268
|
+
console.log(
|
|
269
|
+
` ${r.name.padEnd(20)} ${r.managed ? "managed" : "dev-managed"}`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
console.log(
|
|
273
|
+
"\n Flip any repo between managed and dev-managed in the web UX" +
|
|
274
|
+
`\n (or \`${product().cli} use-local <repo>\`); tests pick the change up automatically.` +
|
|
275
|
+
"\n Sibling-path registration for hosted setups: make register-repos args=--legacy\n",
|
|
276
|
+
);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (plan.mode === "defaults") {
|
|
281
|
+
console.log(`\n ${product().cli} · Test repo sources\n`);
|
|
282
|
+
console.log(
|
|
283
|
+
" Local Dev is enabled but no workspace exists yet. Your first" +
|
|
284
|
+
` \`${product().cli}\` command\n seeds the default workspace with every standard` +
|
|
285
|
+
` system repo, all managed\n out of the box (${plan.repos.length} repos):\n`,
|
|
286
|
+
);
|
|
287
|
+
console.log(` ${plan.repos.join(", ")}\n`);
|
|
288
|
+
console.log(
|
|
289
|
+
" Test discovery follows that workspace automatically — nothing to register.\n",
|
|
290
|
+
);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await legacyScanAndSelect();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Direct-run guard: jest imports this module for planRegistration — only run
|
|
298
|
+
// the CLI when executed as a script (jest's argv[1] is the jest binary).
|
|
299
|
+
const isDirectRun = Boolean(
|
|
300
|
+
process.argv[1] && /register-repos\.(ts|js)$/.test(process.argv[1]),
|
|
301
|
+
);
|
|
302
|
+
if (isDirectRun) {
|
|
303
|
+
main().catch((err) => {
|
|
304
|
+
console.error(err);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
});
|
|
307
|
+
}
|