@monster-orange/den 0.0.0 → 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.
Files changed (231) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +61 -0
  5. package/docs/distributions.md +256 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +99 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +406 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +108 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +729 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/applications.ts +138 -0
  91. package/src/shared/auth/api-token.ts +416 -0
  92. package/src/shared/auth/auth0-helper.ts +284 -0
  93. package/src/shared/auth/headless-login.ts +265 -0
  94. package/src/shared/auth/jwt-helper.ts +186 -0
  95. package/src/shared/auth/sudo-vault.ts +69 -0
  96. package/src/shared/auth/test-accounts.ts +98 -0
  97. package/src/shared/auth/token-injection.ts +184 -0
  98. package/src/shared/auth/token-providers/auth0.ts +80 -0
  99. package/src/shared/auth/token-providers/index.ts +182 -0
  100. package/src/shared/auth/token-refresh.ts +273 -0
  101. package/src/shared/banner.ts +15 -0
  102. package/src/shared/brand.ts +79 -0
  103. package/src/shared/cli-tabs.ts +131 -0
  104. package/src/shared/constants.ts +128 -0
  105. package/src/shared/den-config.ts +566 -0
  106. package/src/shared/dev-metrics/emit.ts +354 -0
  107. package/src/shared/edge/config.ts +229 -0
  108. package/src/shared/edge/render.ts +221 -0
  109. package/src/shared/edge/runtime.ts +268 -0
  110. package/src/shared/edge/types.ts +62 -0
  111. package/src/shared/engine.ts +67 -0
  112. package/src/shared/environment/aws-profiles.ts +49 -0
  113. package/src/shared/environment/env-selector.ts +161 -0
  114. package/src/shared/environment/playwright-env.ts +82 -0
  115. package/src/shared/environment-manager/catalog.ts +77 -0
  116. package/src/shared/environment-manager/clone-guard.ts +163 -0
  117. package/src/shared/environment-manager/dag.ts +286 -0
  118. package/src/shared/environment-manager/default-workspace.ts +182 -0
  119. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  120. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  121. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  122. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  123. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  124. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  125. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  126. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  127. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  128. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  129. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  130. package/src/shared/environment-manager/derivation/text.ts +40 -0
  131. package/src/shared/environment-manager/derivation/types.ts +69 -0
  132. package/src/shared/environment-manager/health.ts +589 -0
  133. package/src/shared/environment-manager/index.ts +6 -0
  134. package/src/shared/environment-manager/load-applications.ts +78 -0
  135. package/src/shared/environment-manager/log-tail.ts +212 -0
  136. package/src/shared/environment-manager/make-targets.ts +78 -0
  137. package/src/shared/environment-manager/opctl.ts +334 -0
  138. package/src/shared/environment-manager/readiness.ts +93 -0
  139. package/src/shared/environment-manager/register-local.ts +72 -0
  140. package/src/shared/environment-manager/secrets-check.ts +260 -0
  141. package/src/shared/environment-manager/service-env.ts +110 -0
  142. package/src/shared/environment-manager/service-runner.ts +1588 -0
  143. package/src/shared/environment-manager/up-plan.ts +302 -0
  144. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  145. package/src/shared/environment-manager/workspaces.ts +860 -0
  146. package/src/shared/manifest/deps.ts +27 -0
  147. package/src/shared/manifest/migrate-manifest.ts +101 -0
  148. package/src/shared/manifest/read-manifest.ts +160 -0
  149. package/src/shared/manifest/schema.ts +299 -0
  150. package/src/shared/manifest/service-links.ts +93 -0
  151. package/src/shared/manifest/validate-manifest.ts +140 -0
  152. package/src/shared/manifest/workspace-manifests.ts +92 -0
  153. package/src/shared/open-url.ts +122 -0
  154. package/src/shared/org.ts +44 -0
  155. package/src/shared/paths.ts +178 -0
  156. package/src/shared/perf/optional-fields.ts +105 -0
  157. package/src/shared/perf/report-normalizer.ts +404 -0
  158. package/src/shared/perf/report-paths.ts +19 -0
  159. package/src/shared/perf/report-store.ts +110 -0
  160. package/src/shared/perf/report-types.ts +70 -0
  161. package/src/shared/perf/test-paths.ts +25 -0
  162. package/src/shared/process/log-rotation.ts +147 -0
  163. package/src/shared/process/spawn-streaming.ts +229 -0
  164. package/src/shared/product.ts +92 -0
  165. package/src/shared/profile.ts +141 -0
  166. package/src/shared/repo-kind.ts +32 -0
  167. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  168. package/src/shared/reporting/slack-reporter.ts +340 -0
  169. package/src/shared/results/result-reader.ts +266 -0
  170. package/src/shared/results/result-writer.ts +168 -0
  171. package/src/shared/secrets/doppler.ts +126 -0
  172. package/src/shared/secrets/env.ts +48 -0
  173. package/src/shared/secrets/index.ts +197 -0
  174. package/src/shared/secrets/keys.ts +33 -0
  175. package/src/shared/secrets/onepassword.ts +75 -0
  176. package/src/shared/secrets/types.ts +52 -0
  177. package/src/shared/service-url.ts +119 -0
  178. package/src/shared/smoke/branch-resolver.ts +141 -0
  179. package/src/shared/smoke/git-remote.ts +70 -0
  180. package/src/shared/smoke/repo-cloner.ts +151 -0
  181. package/src/shared/stacks/config.ts +137 -0
  182. package/src/shared/stacks/manifest-commands.ts +88 -0
  183. package/src/shared/stacks/render.ts +253 -0
  184. package/src/shared/stacks/runtime.ts +334 -0
  185. package/src/shared/stacks/types.ts +53 -0
  186. package/src/shared/terminal/tabs.ts +212 -0
  187. package/src/shared/test-discovery/module-scanner.ts +569 -0
  188. package/src/shared/types/index.ts +678 -0
  189. package/src/shared/ui-marker.ts +19 -0
  190. package/src/ui/cli/applications-command.ts +116 -0
  191. package/src/ui/cli/config-command.ts +68 -0
  192. package/src/ui/cli/create-main.ts +9 -0
  193. package/src/ui/cli/create.ts +360 -0
  194. package/src/ui/cli/edge-command.ts +91 -0
  195. package/src/ui/cli/flag-parser.ts +246 -0
  196. package/src/ui/cli/graph-command.ts +524 -0
  197. package/src/ui/cli/health-display.ts +63 -0
  198. package/src/ui/cli/index.ts +3656 -0
  199. package/src/ui/cli/init.ts +855 -0
  200. package/src/ui/cli/invocation-target.ts +175 -0
  201. package/src/ui/cli/logs-command.ts +104 -0
  202. package/src/ui/cli/module-selection.ts +31 -0
  203. package/src/ui/cli/prompts.ts +767 -0
  204. package/src/ui/cli/results-viewer.ts +436 -0
  205. package/src/ui/cli/secrets-preflight.ts +91 -0
  206. package/src/ui/cli/stack-command.ts +144 -0
  207. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  208. package/src/ui/cli/test-runner.ts +1312 -0
  209. package/src/ui/cli/web-command.ts +1073 -0
  210. package/src/ui/server/changelog-parser.ts +94 -0
  211. package/src/ui/server/daemon-reaper.ts +129 -0
  212. package/src/ui/server/editor-manager.ts +301 -0
  213. package/src/ui/server/github-service.ts +223 -0
  214. package/src/ui/server/http-helpers.ts +98 -0
  215. package/src/ui/server/index.ts +3236 -0
  216. package/src/ui/server/log-history.ts +452 -0
  217. package/src/ui/server/perf-report.ts +42 -0
  218. package/src/ui/server/public/environments.html +494 -0
  219. package/src/ui/server/public/favicon.ico +0 -0
  220. package/src/ui/server/public/header.js +1371 -0
  221. package/src/ui/server/public/index.html +2595 -0
  222. package/src/ui/server/public/local-dev.html +7405 -0
  223. package/src/ui/server/public/logs.html +1489 -0
  224. package/src/ui/server/public/perf-report.js +359 -0
  225. package/src/ui/server/public/release.html +2111 -0
  226. package/src/ui/server/public/services.html +1098 -0
  227. package/src/ui/server/public/workspace-picker.js +69 -0
  228. package/src/ui/server/release-deploy-session.ts +246 -0
  229. package/src/ui/server/release-service.ts +1125 -0
  230. package/src/ui/server/sudo-store.ts +111 -0
  231. package/src/ui/server/workspace-routes.ts +2154 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Playwright Configuration — All Smoke Tests, All Repos
3
+ *
4
+ * Used by: make test-smk-global
5
+ *
6
+ * Creates one Playwright project per service per registered repo.
7
+ * All results appear in a single combined report.
8
+ *
9
+ * Environment variables:
10
+ * E2E_ENVIRONMENT — env config to load (default: local)
11
+ * BASE_API_URL — override API base URL
12
+ */
13
+
14
+ import { defineConfig } from "@playwright/test";
15
+ import * as fs from "fs";
16
+ import * as path from "path";
17
+ import { findManifest } from "./discover.js";
18
+ import {
19
+ resolveManifestSmokeBaseUrl,
20
+ resolveSmokeBaseUrl,
21
+ } from "./base-url.js";
22
+ import type { Environment } from "../../shared/types/index.js";
23
+ import { getDistRoot } from "../../shared/paths.js";
24
+ import { distPath, enginePath } from "../../shared/engine.js";
25
+
26
+ import { product } from "../../shared/product.js";
27
+ // Den-root anchored in lockstep with setup-global.ts (the manifest writer).
28
+ const PROJECT_ROOT = getDistRoot();
29
+ const SMOKE_RUN_DIR = path.resolve(PROJECT_ROOT, ".smoke-run");
30
+ const environment = (process.env.E2E_ENVIRONMENT || "local") as Environment;
31
+
32
+ const configPath = path.resolve(
33
+ PROJECT_ROOT,
34
+ `config/environments/${environment}.json`,
35
+ );
36
+ let envConfig: { baseUrls: { api: string; web: string } };
37
+ try {
38
+ envConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
39
+ } catch {
40
+ console.error(
41
+ `\n❌ playwright-global.config: no env config for "${environment}"`,
42
+ );
43
+ console.error(` Expected: config/environments/${environment}.json\n`);
44
+ process.exit(1);
45
+ }
46
+
47
+ // Read exactly what setup-global.ts staged — no second discoverRepos() call
48
+ const globalManifestPath = path.join(SMOKE_RUN_DIR, "global-manifest.json");
49
+ if (!fs.existsSync(globalManifestPath)) {
50
+ console.error(
51
+ `\n❌ No staged smoke tests found. Run: make test-smk-global\n`,
52
+ );
53
+ process.exit(1);
54
+ }
55
+ const staged: Array<{
56
+ repo: string;
57
+ svc: string;
58
+ dir: string;
59
+ testMatch: string;
60
+ baseUrlKey: string;
61
+ }> = JSON.parse(fs.readFileSync(globalManifestPath, "utf-8"));
62
+
63
+ if (!staged.length) {
64
+ console.error(
65
+ `\n❌ No smoke services staged. Check your ${product().manifestYml} smoke: keys.\n`,
66
+ );
67
+ process.exit(1);
68
+ }
69
+
70
+ const projects = staged.map(({ repo, svc, dir, testMatch, baseUrlKey }) => {
71
+ if (!fs.existsSync(dir)) {
72
+ console.error(`\n❌ Staging dir missing: ${dir}`);
73
+ console.error(` Re-run: make test-smk-global\n`);
74
+ process.exit(1);
75
+ }
76
+
77
+ const configuredUrl =
78
+ baseUrlKey === "web" ? envConfig.baseUrls.web : envConfig.baseUrls.api;
79
+ const discovered = findManifest(repo);
80
+ if (!discovered) {
81
+ console.error(
82
+ `\n❌ No manifest found for repo "${repo}" in .${product().name}/`,
83
+ );
84
+ console.error(` Run: make register-repos\n`);
85
+ process.exit(1);
86
+ }
87
+ const manifestUrl = resolveManifestSmokeBaseUrl({
88
+ manifest: discovered.manifest,
89
+ repoName: repo,
90
+ serviceName: svc,
91
+ environment,
92
+ baseUrlKey: baseUrlKey === "web" ? "web" : "api",
93
+ });
94
+ const baseUrl = resolveSmokeBaseUrl({
95
+ baseUrlKey: baseUrlKey === "web" ? "web" : "api",
96
+ configuredUrl,
97
+ manifestUrl,
98
+ overrideUrl: process.env.BASE_API_URL,
99
+ });
100
+
101
+ return {
102
+ name: `${repo}/${svc}`,
103
+ testDir: dir,
104
+ testMatch,
105
+ use: {
106
+ baseURL: baseUrl,
107
+ timeout: 30000,
108
+ },
109
+ };
110
+ });
111
+
112
+ export default defineConfig({
113
+ globalSetup: enginePath("src/pillars/smoke/global-setup-global.ts"),
114
+ globalTeardown: enginePath("src/pillars/smoke/global-teardown-global.ts"),
115
+
116
+ fullyParallel: true,
117
+ forbidOnly: !!process.env.CI,
118
+ retries: process.env.CI ? 2 : 0,
119
+ workers: process.env.CI ? 1 : undefined,
120
+
121
+ reporter: [
122
+ [
123
+ "html",
124
+ {
125
+ outputFolder:
126
+ process.env.PLAYWRIGHT_HTML_REPORT || distPath("playwright-report"),
127
+ open: "never",
128
+ },
129
+ ],
130
+ ["list"],
131
+ [
132
+ "junit",
133
+ { outputFile: distPath("reports", "junit-smoke-global-results.xml") },
134
+ ],
135
+ ],
136
+
137
+ projects,
138
+
139
+ timeout: 60000,
140
+ expect: { timeout: 10000 },
141
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Playwright Configuration for Smoke Tests
3
+ *
4
+ * Dynamically configured from the service's .den.json manifest.
5
+ * The testDir, testMatch, and baseURL are all resolved at config time
6
+ * from the manifest discovered in .den/<repo>/.den.json.
7
+ *
8
+ * Environment variables:
9
+ * SMOKE_SERVICE — service name in the manifest (default: api)
10
+ * SMOKE_REPO — repo name in .den/ (required)
11
+ * E2E_ENVIRONMENT — env config to load (default: local)
12
+ * BASE_API_URL — override base URL (useful for one-off runs)
13
+ */
14
+
15
+ import { defineConfig } from "@playwright/test";
16
+ import * as fs from "fs";
17
+ import * as path from "path";
18
+ import { findManifest, resolveService } from "./discover.js";
19
+ import { getDistRoot } from "../../shared/paths.js";
20
+ import { distPath, enginePath } from "../../shared/engine.js";
21
+ import {
22
+ resolveManifestSmokeBaseUrl,
23
+ resolveSmokeBaseUrl,
24
+ } from "./base-url.js";
25
+ import { smokeRepoName } from "./smoke-repo.js";
26
+
27
+ import { product } from "../../shared/product.js";
28
+ // Den-root anchored in lockstep with setup.ts (the .smoke-run writer).
29
+ const PROJECT_ROOT = getDistRoot();
30
+
31
+ function resolveConfig() {
32
+ const serviceName = process.env.SMOKE_SERVICE || "api";
33
+ const repoName = smokeRepoName();
34
+ const environment = process.env.E2E_ENVIRONMENT || "local";
35
+
36
+ // Discover the repo manifest
37
+ const discovered = findManifest(repoName);
38
+ if (!discovered) {
39
+ console.error(
40
+ `\n❌ Smoke config: no manifest found for repo "${repoName}" in .${product().name}/`,
41
+ );
42
+ console.error(` Run: make smoke-link repo=${repoName}\n`);
43
+ process.exit(1);
44
+ }
45
+
46
+ const svc = resolveService(discovered, serviceName);
47
+ if (!svc) {
48
+ const available = Object.keys(discovered.manifest.smoke ?? {}).join(", ");
49
+ console.error(
50
+ `\n❌ Smoke config: service "${serviceName}" not found in ${repoName}/${product().manifestJson}`,
51
+ );
52
+ console.error(` Available services: ${available}\n`);
53
+ process.exit(1);
54
+ }
55
+
56
+ // Load environment config for the base URL
57
+ const configPath = path.resolve(
58
+ PROJECT_ROOT,
59
+ `config/environments/${environment}.json`,
60
+ );
61
+ let envConfig: { baseUrls: { api: string; web: string } };
62
+ try {
63
+ envConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
64
+ } catch {
65
+ console.error(
66
+ `\n❌ Smoke config: no env config found for "${environment}"`,
67
+ );
68
+ console.error(` Expected: config/environments/${environment}.json\n`);
69
+ process.exit(1);
70
+ }
71
+
72
+ const configuredUrl =
73
+ svc.baseUrlKey === "api" ? envConfig.baseUrls.api : envConfig.baseUrls.web;
74
+ const manifestUrl = resolveManifestSmokeBaseUrl({
75
+ manifest: discovered.manifest,
76
+ repoName,
77
+ serviceName,
78
+ environment: environment as
79
+ | "local"
80
+ | "dev"
81
+ | "sandbox"
82
+ | "staging"
83
+ | "production",
84
+ baseUrlKey: svc.baseUrlKey,
85
+ });
86
+ const baseUrl = resolveSmokeBaseUrl({
87
+ baseUrlKey: svc.baseUrlKey,
88
+ configuredUrl,
89
+ manifestUrl,
90
+ overrideUrl: process.env.BASE_API_URL,
91
+ });
92
+
93
+ // Use the staged test dir (.smoke-run/{repo}-{service}/) which has a
94
+ // node_modules shim pointing to den's playwright. This prevents the
95
+ // "Requiring @playwright/test second time" error caused by the repo's own
96
+ // @playwright/test in node_modules being found during test file loading.
97
+ const stagingDir = path.resolve(
98
+ PROJECT_ROOT,
99
+ `.smoke-run/${repoName}-${serviceName}`,
100
+ );
101
+ if (!fs.existsSync(stagingDir)) {
102
+ console.error(
103
+ `\n❌ Staging dir not found: .smoke-run/${repoName}-${serviceName}/`,
104
+ );
105
+ console.error(
106
+ ` The setup step runs automatically via make targets. If running`,
107
+ );
108
+ console.error(
109
+ ` playwright directly, run: SMOKE_SERVICE=${serviceName} SMOKE_REPO=${repoName} npx tsx src/pillars/smoke/setup.ts\n`,
110
+ );
111
+ process.exit(1);
112
+ }
113
+
114
+ return { svc: { ...svc, testDir: stagingDir }, baseUrl };
115
+ }
116
+
117
+ const { svc, baseUrl } = resolveConfig();
118
+
119
+ export default defineConfig({
120
+ testDir: svc.testDir,
121
+ testMatch: svc.testMatch,
122
+
123
+ globalSetup: enginePath("src/pillars/smoke/global-setup.ts"),
124
+ globalTeardown: enginePath("src/pillars/smoke/global-teardown.ts"),
125
+
126
+ fullyParallel: true,
127
+ forbidOnly: !!process.env.CI,
128
+ retries: process.env.CI ? 2 : 0,
129
+ workers: process.env.CI ? 1 : undefined,
130
+
131
+ reporter: [
132
+ [
133
+ "html",
134
+ {
135
+ outputFolder:
136
+ process.env.PLAYWRIGHT_HTML_REPORT || distPath("playwright-report"),
137
+ open: "never",
138
+ },
139
+ ],
140
+ ["list"],
141
+ ["junit", { outputFile: distPath("reports", "junit-smoke-results.xml") }],
142
+ ],
143
+
144
+ use: {
145
+ baseURL: baseUrl,
146
+ // JSON headers only for API services; web tests use a headless browser
147
+ ...(svc.baseUrlKey === "api"
148
+ ? {
149
+ extraHTTPHeaders: {
150
+ Accept: "application/json",
151
+ "Content-Type": "application/json",
152
+ },
153
+ }
154
+ : {}),
155
+ timeout: 30000,
156
+ },
157
+
158
+ projects: [{ name: "smoke", use: {} }],
159
+
160
+ timeout: 60000,
161
+ expect: { timeout: 10000 },
162
+ });
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Smoke Test Environment Setup
3
+ *
4
+ * For non-local environments: resolves the exact deployed SHA from the GitHub
5
+ * Deployments API and ensures the repo is cloned at that SHA inside
6
+ * .smoke-clones/{repo}/{env}/.
7
+ *
8
+ * For local: no-op — the local sibling repo is already linked via .den/.
9
+ *
10
+ * Usage (called by buildCommand before the staging step):
11
+ * npx tsx src/pillars/smoke/setup-env.ts --env staging [--repo api]
12
+ *
13
+ * Multiple --repo flags are accepted for global runs:
14
+ * npx tsx src/pillars/smoke/setup-env.ts --env staging --repo api --repo other-repo
15
+ *
16
+ * If --repo is omitted, all registered repos with smoke tests are resolved.
17
+ */
18
+
19
+ import { discoverRepos, findManifest } from "./discover.js";
20
+ import { resolveDeployedRef } from "../../shared/smoke/branch-resolver.js";
21
+ import { ensureRepoAtSha } from "../../shared/smoke/repo-cloner.js";
22
+ import {
23
+ getGitHubCoords,
24
+ getGitRemoteOrigin,
25
+ } from "../../shared/smoke/git-remote.js";
26
+ import type { Environment } from "../../shared/types/index.js";
27
+
28
+ const LOCAL_ENV: Environment = "local";
29
+
30
+ function parseArgs(): { environment: Environment; repoNames: string[] } {
31
+ const args = process.argv.slice(2);
32
+ let environment: Environment = LOCAL_ENV;
33
+ const repoNames: string[] = [];
34
+
35
+ for (let i = 0; i < args.length; i++) {
36
+ if (args[i] === "--env" && args[i + 1]) {
37
+ environment = args[++i] as Environment;
38
+ } else if (args[i] === "--repo" && args[i + 1]) {
39
+ repoNames.push(args[++i]);
40
+ }
41
+ }
42
+
43
+ return { environment, repoNames };
44
+ }
45
+
46
+ async function main() {
47
+ const { environment, repoNames } = parseArgs();
48
+
49
+ if (environment === LOCAL_ENV) {
50
+ // Nothing to do — local tests use the sibling repo via .den/ symlink
51
+ console.log(" ℹ local environment: using linked repo (no clone needed)");
52
+ return;
53
+ }
54
+
55
+ // Gather repos to resolve
56
+ let repos = discoverRepos().filter(
57
+ (r) =>
58
+ Object.keys(r.manifest.smoke ?? {}).length > 0 || !!r.manifest.synthetics,
59
+ );
60
+
61
+ if (repoNames.length > 0) {
62
+ const filtered = repoNames.map((name) => {
63
+ const found = findManifest(name);
64
+ if (!found) {
65
+ console.error(`❌ Repo "${name}" not found. Run: make register-repos`);
66
+ process.exit(1);
67
+ }
68
+ return found;
69
+ });
70
+ repos = filtered;
71
+ }
72
+
73
+ if (!repos.length) {
74
+ console.error(
75
+ "❌ No repos with smoke tests registered. Run: make register-repos",
76
+ );
77
+ process.exit(1);
78
+ }
79
+
80
+ console.log(
81
+ `\n🔍 Resolving deployed branch for ${repos.map((r) => r.repoName).join(", ")} @ ${environment}\n`,
82
+ );
83
+
84
+ for (const repo of repos) {
85
+ // Derive GitHub owner/repo from the local repo's git remote
86
+ const coords = getGitHubCoords(repo.repoPath);
87
+ if (!coords) {
88
+ console.error(
89
+ `❌ Cannot determine GitHub owner/repo for "${repo.repoName}". ` +
90
+ `No git remote origin found at ${repo.repoPath}`,
91
+ );
92
+ process.exit(1);
93
+ }
94
+
95
+ // Allow .den.yml to override the GitHub environment name
96
+ const githubEnv =
97
+ repo.manifest.github?.environments?.[environment] ?? environment;
98
+
99
+ let ref;
100
+ try {
101
+ ref = await resolveDeployedRef(coords.owner, coords.repo, githubEnv);
102
+ } catch (err) {
103
+ console.error(`❌ ${(err as Error).message}`);
104
+ process.exit(1);
105
+ }
106
+
107
+ console.log(
108
+ ` 📦 ${repo.repoName}: ${coords.owner}/${coords.repo}@${githubEnv} → ` +
109
+ `${ref.sha.slice(0, 8)} (${ref.ref}, deployed ${new Date(ref.deployedAt).toLocaleString()})`,
110
+ );
111
+
112
+ // Get the git remote URL from the local symlinked/registered copy
113
+ const originUrl = getGitRemoteOrigin(repo.repoPath);
114
+ if (!originUrl) {
115
+ console.error(
116
+ `❌ Cannot get git remote origin URL for "${repo.repoName}" at ${repo.repoPath}`,
117
+ );
118
+ process.exit(1);
119
+ }
120
+
121
+ ensureRepoAtSha(repo.repoName, environment, originUrl, ref);
122
+ }
123
+
124
+ console.log(
125
+ `\n✅ All repos cloned/updated for environment "${environment}"\n`,
126
+ );
127
+ }
128
+
129
+ main().catch((err) => {
130
+ console.error(err);
131
+ process.exit(1);
132
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Smoke Test Staging Setup — All Repos
3
+ *
4
+ * Stages smoke test files for every service in every registered repo.
5
+ * Used by: make test-smk-global
6
+ */
7
+
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ import { discoverRepos, resolveRepoPath, resolveService } from "./discover.js";
11
+ import { stageSmokeTree } from "./staging.js";
12
+ import type { Environment } from "../../shared/types/index.js";
13
+ import { getDistRoot } from "../../shared/paths.js";
14
+
15
+ // Den-root anchored, NOT process.cwd() — bulk writer of .smoke-run (see setup.ts).
16
+ const PROJECT_ROOT = getDistRoot();
17
+ const SMOKE_RUN_DIR = path.resolve(PROJECT_ROOT, ".smoke-run");
18
+ const DEN_MODULES = path.resolve(PROJECT_ROOT, "node_modules");
19
+
20
+ /** Separator used in staging dir names — must not appear in repo or service names. */
21
+ export const STAGING_SEP = "__";
22
+
23
+ /** Canonical staging dir for a repo+service pair. */
24
+ export function stagingDirFor(repoName: string, svcName: string): string {
25
+ return path.join(SMOKE_RUN_DIR, `${repoName}${STAGING_SEP}${svcName}`);
26
+ }
27
+
28
+ async function main() {
29
+ const environment = (process.env.E2E_ENVIRONMENT || "local") as Environment;
30
+ const useLocalCheckout = process.env.USE_LOCAL_CHECKOUT === "1";
31
+ const repos = discoverRepos();
32
+ if (!repos.length) {
33
+ console.error("❌ No repos registered. Run: make register-repos");
34
+ process.exit(1);
35
+ }
36
+
37
+ let total = 0;
38
+ const staged: Array<{
39
+ repo: string;
40
+ svc: string;
41
+ dir: string;
42
+ testMatch: string;
43
+ baseUrlKey: string;
44
+ }> = [];
45
+
46
+ for (const repo of repos) {
47
+ const services = Object.keys(repo.manifest.smoke ?? {});
48
+ if (!services.length) continue;
49
+
50
+ // Resolve the staging root for this run: deployed clone by default for
51
+ // cloud envs, or the registered checkout when explicitly requested.
52
+ let repoPath: string;
53
+ try {
54
+ repoPath = resolveRepoPath(repo, environment, useLocalCheckout);
55
+ } catch (err) {
56
+ console.error(`❌ ${(err as Error).message}`);
57
+ process.exit(1);
58
+ }
59
+ const envRepo = { ...repo, repoPath };
60
+
61
+ for (const svcName of services) {
62
+ const svc = resolveService(envRepo, svcName);
63
+ if (!svc) continue;
64
+
65
+ const stagingDir = stagingDirFor(repo.repoName, svcName);
66
+ fs.rmSync(stagingDir, { recursive: true, force: true });
67
+ fs.mkdirSync(stagingDir, { recursive: true });
68
+
69
+ // Node modules shim — point @playwright/test to den's copy
70
+ const pwScopeDir = path.join(stagingDir, "node_modules", "@playwright");
71
+ fs.mkdirSync(pwScopeDir, { recursive: true });
72
+ fs.symlinkSync(
73
+ path.join(DEN_MODULES, "@playwright", "test"),
74
+ path.join(pwScopeDir, "test"),
75
+ );
76
+ fs.symlinkSync(
77
+ path.join(DEN_MODULES, "playwright"),
78
+ path.join(stagingDir, "node_modules", "playwright"),
79
+ );
80
+
81
+ let count = 0;
82
+ try {
83
+ const result = stageSmokeTree(svc.testDir, stagingDir, svc.testMatch);
84
+ count = result.matchedTestFiles;
85
+ if (result.matchedTestFiles === 0) {
86
+ console.warn(
87
+ ` ⚠ No files matching "${svc.testMatch}" in ${svc.testDir}`,
88
+ );
89
+ }
90
+ console.log(
91
+ `✅ ${repo.repoName}/${svcName}: staged ${result.matchedTestFiles} smoke test file(s) and ${result.copiedFiles} total file(s) → ${path.basename(stagingDir)}/`,
92
+ );
93
+ } catch (error) {
94
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
95
+ console.warn(` ⚠ testDir not found, skipping: ${svc.testDir}`);
96
+ continue;
97
+ }
98
+ throw error;
99
+ }
100
+ staged.push({
101
+ repo: repo.repoName,
102
+ svc: svcName,
103
+ dir: stagingDir,
104
+ testMatch: svc.testMatch,
105
+ baseUrlKey: svc.baseUrlKey ?? "api",
106
+ });
107
+ total += count;
108
+ }
109
+ }
110
+
111
+ // Write a manifest so playwright-global.config.ts reads exactly what was staged
112
+ // (avoids a second discoverRepos() call that could diverge)
113
+ fs.writeFileSync(
114
+ path.join(SMOKE_RUN_DIR, "global-manifest.json"),
115
+ JSON.stringify(staged, null, 2),
116
+ );
117
+
118
+ console.log(
119
+ `\n Total: ${total} smoke test file(s) staged across ${repos.length} repo(s)\n`,
120
+ );
121
+ }
122
+
123
+ main().catch((err) => {
124
+ console.error(err);
125
+ process.exit(1);
126
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Smoke Test Staging Setup
3
+ *
4
+ * Creates .smoke-run/{repo}-{service}/ before each test run:
5
+ *
6
+ * 1. Copies all *.smoke.ts files from the repo's testDir
7
+ * 2. Creates node_modules/@playwright/test → den's playwright
8
+ * 3. Creates node_modules/playwright → den's playwright
9
+ *
10
+ * Why: service repos usually have their own @playwright/test in
11
+ * node_modules. If Playwright's test runner loads test files from those
12
+ * directories, Node resolves @playwright/test to the repo's copy — which is
13
+ * a different module instance than the runner's, causing the "Requiring
14
+ * @playwright/test second time" error.
15
+ *
16
+ * Staging the files inside den (with a node_modules shim) ensures
17
+ * there is exactly one @playwright/test instance: den's.
18
+ *
19
+ * The staged dir is gitignored (.smoke-run/) and re-created fresh each run.
20
+ */
21
+
22
+ import * as fs from "fs";
23
+ import * as path from "path";
24
+ import { findManifest, resolveRepoPath, resolveService } from "./discover.js";
25
+ import { stageSmokeTree } from "./staging.js";
26
+ import type { Environment } from "../../shared/types/index.js";
27
+ import { getDistRoot } from "../../shared/paths.js";
28
+ import { smokeRepoName } from "./smoke-repo.js";
29
+
30
+ import { product } from "../../shared/product.js";
31
+ // Den-root anchored, NOT process.cwd(): this stages (rm -rf + mkdir +
32
+ // symlink + tree-copy) .smoke-run, and CLI-spawned runs inherit the user's cwd.
33
+ const PROJECT_ROOT = getDistRoot();
34
+ const SMOKE_RUN_DIR = path.resolve(PROJECT_ROOT, ".smoke-run");
35
+ const ENGINE_MODULES = path.resolve(PROJECT_ROOT, "node_modules");
36
+
37
+ async function main() {
38
+ const serviceName = process.env.SMOKE_SERVICE || "api";
39
+ const repoName = smokeRepoName();
40
+ const environment = (process.env.E2E_ENVIRONMENT || "local") as Environment;
41
+ const useLocalCheckout = process.env.USE_LOCAL_CHECKOUT === "1";
42
+
43
+ const discovered = findManifest(repoName);
44
+ if (!discovered) {
45
+ console.error(
46
+ `❌ No manifest found for repo "${repoName}" in .${product().name}/`,
47
+ );
48
+ console.error(` Run: make smoke-link repo=${repoName}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ // Non-local smoke runs stage from the deployed clone by default. Set
53
+ // USE_LOCAL_CHECKOUT=1 to stage from the registered checkout instead.
54
+ let repoPath: string;
55
+ try {
56
+ repoPath = resolveRepoPath(discovered, environment, useLocalCheckout);
57
+ } catch (err) {
58
+ console.error(`❌ ${(err as Error).message}`);
59
+ process.exit(1);
60
+ }
61
+
62
+ // Re-resolve the service from the selected staging root so testDir points
63
+ // into either the deployed clone or the registered checkout.
64
+ const envDiscovered = { ...discovered, repoPath };
65
+ const svc = resolveService(envDiscovered, serviceName);
66
+ if (!svc) {
67
+ const available = Object.keys(discovered.manifest.smoke ?? {}).join(", ");
68
+ console.error(
69
+ `❌ Service "${serviceName}" not found in ${repoName}/${product().manifestJson}`,
70
+ );
71
+ console.error(` Available: ${available}`);
72
+ process.exit(1);
73
+ }
74
+
75
+ const stagingDir = path.join(SMOKE_RUN_DIR, `${repoName}-${serviceName}`);
76
+
77
+ // Clean and recreate staging dir
78
+ fs.rmSync(stagingDir, { recursive: true, force: true });
79
+ fs.mkdirSync(stagingDir, { recursive: true });
80
+
81
+ // ── Node modules shim ──────────────────────────────────────────────────
82
+ // Symlink den's playwright into the staging dir's node_modules so
83
+ // that any @playwright/test import in copied test files resolves to the
84
+ // runner's copy, not the repo's own copy.
85
+ const nmDir = path.join(stagingDir, "node_modules");
86
+ const pwScopeDir = path.join(nmDir, "@playwright");
87
+ fs.mkdirSync(pwScopeDir, { recursive: true });
88
+
89
+ const pwTestSrc = path.join(ENGINE_MODULES, "@playwright", "test");
90
+ const pwTestDst = path.join(pwScopeDir, "test");
91
+ fs.symlinkSync(pwTestSrc, pwTestDst);
92
+
93
+ const pwSrc = path.join(ENGINE_MODULES, "playwright");
94
+ const pwDst = path.join(nmDir, "playwright");
95
+ fs.symlinkSync(pwSrc, pwDst);
96
+
97
+ // ── Copy test tree ─────────────────────────────────────────────────────
98
+ // Copy the full testDir so smoke tests can import shared helper modules
99
+ // that do not themselves match `testMatch`.
100
+ const result = stageSmokeTree(svc.testDir, stagingDir, svc.testMatch);
101
+ if (result.matchedTestFiles === 0) {
102
+ console.warn(
103
+ ` ⚠ No files matching "${svc.testMatch}" found in ${svc.testDir}`,
104
+ );
105
+ }
106
+
107
+ console.log(
108
+ `✅ Staged ${result.matchedTestFiles} smoke test file(s) and ${result.copiedFiles} total file(s) → .smoke-run/${repoName}-${serviceName}/`,
109
+ );
110
+ }
111
+
112
+ main().catch((err) => {
113
+ console.error(err);
114
+ process.exit(1);
115
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The repo a smoke run targets. Always named by the caller (`make
3
+ * test-smoke repo=<name>` / the web UI set SMOKE_REPO); the engine has no
4
+ * default repo, so a missing name fails loudly instead of guessing one.
5
+ */
6
+ export function smokeRepoName(env: NodeJS.ProcessEnv = process.env): string {
7
+ const name = env.SMOKE_REPO?.trim();
8
+ if (!name) {
9
+ console.error(
10
+ "\n❌ SMOKE_REPO is not set: name the repo whose smoke tests to run " +
11
+ "(make test-smoke repo=<name>).\n",
12
+ );
13
+ process.exit(1);
14
+ }
15
+ return name;
16
+ }