@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.
- package/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- 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 +99 -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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test repo cloner
|
|
3
|
+
*
|
|
4
|
+
* Manages .smoke-clones/{repoName}/{environment}/ — shallow clones of
|
|
5
|
+
* registered repos checked out at the exact SHA deployed to each environment.
|
|
6
|
+
*
|
|
7
|
+
* Directory layout:
|
|
8
|
+
* .smoke-clones/
|
|
9
|
+
* api/
|
|
10
|
+
* staging/ ← shallow clone at deployed SHA
|
|
11
|
+
* .deployed-sha ← cached SHA (avoids redundant re-clones)
|
|
12
|
+
*
|
|
13
|
+
* Freshness strategy:
|
|
14
|
+
* - Read .deployed-sha from disk
|
|
15
|
+
* - If it matches the target SHA → already up to date, return path immediately
|
|
16
|
+
* - If it differs (or file absent) → fetch + checkout + update stamp
|
|
17
|
+
*
|
|
18
|
+
* Git authentication:
|
|
19
|
+
* Uses `gh auth git-credential` as the git credential helper so the
|
|
20
|
+
* existing `gh` session covers private repos without separate SSH setup.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { execSync } from "child_process";
|
|
24
|
+
import * as fs from "fs";
|
|
25
|
+
import * as path from "path";
|
|
26
|
+
import type { DeployedRef } from "./branch-resolver.js";
|
|
27
|
+
import { getDistRoot } from "../paths.js";
|
|
28
|
+
import { assertSafeCloneDestination } from "../environment-manager/clone-guard.js";
|
|
29
|
+
|
|
30
|
+
// Anchored to the den root, NOT process.cwd() — this module CLONES repos,
|
|
31
|
+
// and a cwd-derived destination once bulk-cloned into an unrelated checkout.
|
|
32
|
+
const CLONES_DIR = path.resolve(getDistRoot(), ".smoke-clones");
|
|
33
|
+
const DEPLOYED_SHA_FILE = ".deployed-sha";
|
|
34
|
+
|
|
35
|
+
/** Absolute path to the clone directory for a repo+environment pair. */
|
|
36
|
+
export function cloneDirFor(repoName: string, environment: string): string {
|
|
37
|
+
return path.join(CLONES_DIR, repoName, environment);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function run(cmd: string, cwd?: string): void {
|
|
41
|
+
execSync(cmd, {
|
|
42
|
+
cwd,
|
|
43
|
+
stdio: "inherit",
|
|
44
|
+
env: {
|
|
45
|
+
...process.env,
|
|
46
|
+
// Route git credentials through `gh` so private repos work without SSH
|
|
47
|
+
GIT_CREDENTIAL_HELPER: "!gh auth git-credential",
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function runCapture(cmd: string, cwd?: string): string {
|
|
53
|
+
return execSync(cmd, {
|
|
54
|
+
cwd,
|
|
55
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
GIT_CREDENTIAL_HELPER: "!gh auth git-credential",
|
|
60
|
+
},
|
|
61
|
+
}).trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Ensure the repo is cloned at the given SHA.
|
|
66
|
+
* Returns the absolute path to the clone root.
|
|
67
|
+
*
|
|
68
|
+
* @param repoName Short name (matches .den.yml `name` field, e.g. "api")
|
|
69
|
+
* @param environment Den environment key (e.g. "staging") — used for path only
|
|
70
|
+
* @param originUrl Git remote URL to clone from
|
|
71
|
+
* @param ref Deployed ref from branch-resolver
|
|
72
|
+
*/
|
|
73
|
+
export function ensureRepoAtSha(
|
|
74
|
+
repoName: string,
|
|
75
|
+
environment: string,
|
|
76
|
+
originUrl: string,
|
|
77
|
+
ref: DeployedRef,
|
|
78
|
+
): string {
|
|
79
|
+
const cloneDir = cloneDirFor(repoName, environment);
|
|
80
|
+
const shaFile = path.join(cloneDir, DEPLOYED_SHA_FILE);
|
|
81
|
+
|
|
82
|
+
// Fast path: already at the right SHA
|
|
83
|
+
if (fs.existsSync(shaFile)) {
|
|
84
|
+
const cached = fs.readFileSync(shaFile, "utf-8").trim();
|
|
85
|
+
if (cached === ref.sha) {
|
|
86
|
+
console.log(
|
|
87
|
+
` ✓ ${repoName}@${environment}: already at ${ref.sha.slice(0, 8)} (${ref.ref})`,
|
|
88
|
+
);
|
|
89
|
+
return cloneDir;
|
|
90
|
+
}
|
|
91
|
+
console.log(
|
|
92
|
+
` ↻ ${repoName}@${environment}: updating ${cached.slice(0, 8)} → ${ref.sha.slice(0, 8)} (${ref.ref})`,
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
console.log(
|
|
96
|
+
` ⬇ ${repoName}@${environment}: cloning at ${ref.sha.slice(0, 8)} (${ref.ref})`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!fs.existsSync(cloneDir)) {
|
|
101
|
+
assertSafeCloneDestination(cloneDir, repoName);
|
|
102
|
+
// Initial clone — shallow, blobless (history not needed, just the tree)
|
|
103
|
+
fs.mkdirSync(cloneDir, { recursive: true });
|
|
104
|
+
try {
|
|
105
|
+
run(
|
|
106
|
+
`git clone --filter=blob:none --depth=1 --branch "${ref.ref}" ` +
|
|
107
|
+
`-c credential.helper="!gh auth git-credential" ` +
|
|
108
|
+
`"${originUrl}" "${cloneDir}"`,
|
|
109
|
+
);
|
|
110
|
+
} catch {
|
|
111
|
+
// Branch may not exist by that name (e.g. detached SHA) — try bare clone
|
|
112
|
+
fs.rmSync(cloneDir, { recursive: true, force: true });
|
|
113
|
+
fs.mkdirSync(cloneDir, { recursive: true });
|
|
114
|
+
run(
|
|
115
|
+
`git clone --filter=blob:none --depth=1 ` +
|
|
116
|
+
`-c credential.helper="!gh auth git-credential" ` +
|
|
117
|
+
`"${originUrl}" "${cloneDir}"`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Fetch the exact SHA (in case shallow clone landed on a different commit)
|
|
123
|
+
const currentSha = runCapture("git rev-parse HEAD", cloneDir);
|
|
124
|
+
if (currentSha !== ref.sha) {
|
|
125
|
+
try {
|
|
126
|
+
run(
|
|
127
|
+
`git -c credential.helper="!gh auth git-credential" ` +
|
|
128
|
+
`fetch --depth=1 origin "${ref.sha}"`,
|
|
129
|
+
cloneDir,
|
|
130
|
+
);
|
|
131
|
+
run(`git checkout "${ref.sha}"`, cloneDir);
|
|
132
|
+
} catch {
|
|
133
|
+
// Some git servers don't allow fetching by SHA directly — fall back to
|
|
134
|
+
// fetching the branch and checking out the SHA
|
|
135
|
+
run(
|
|
136
|
+
`git -c credential.helper="!gh auth git-credential" ` +
|
|
137
|
+
`fetch --depth=1 origin "${ref.ref}"`,
|
|
138
|
+
cloneDir,
|
|
139
|
+
);
|
|
140
|
+
run(`git checkout "${ref.sha}"`, cloneDir);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Write stamp file
|
|
145
|
+
fs.writeFileSync(shaFile, ref.sha);
|
|
146
|
+
|
|
147
|
+
console.log(
|
|
148
|
+
` ✓ ${repoName}@${environment}: ready at ${ref.sha.slice(0, 8)}`,
|
|
149
|
+
);
|
|
150
|
+
return cloneDir;
|
|
151
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stacks come from two places:
|
|
3
|
+
* - a repo's own `.den.yml` `compose:` section (preferred), for every
|
|
4
|
+
* repo in the focused workspace;
|
|
5
|
+
* - `stacks:` in config/den.yml, for repos that don't carry a manifest.
|
|
6
|
+
* A manifest stack wins over a config stack of the same name.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as yaml from "yaml";
|
|
11
|
+
import { getDistRoot } from "../paths.js";
|
|
12
|
+
import { readCatalog } from "../environment-manager/catalog.js";
|
|
13
|
+
import {
|
|
14
|
+
getActiveWorkspace,
|
|
15
|
+
getWorkspaceByName,
|
|
16
|
+
listWorkspaceServices,
|
|
17
|
+
} from "../environment-manager/workspaces.js";
|
|
18
|
+
import { readManifest } from "../manifest/read-manifest.js";
|
|
19
|
+
import type { ManifestComposeConfig } from "../types/index.js";
|
|
20
|
+
import { assertStackName, parseTarget } from "./render.js";
|
|
21
|
+
import type { StackConfig } from "./types.js";
|
|
22
|
+
|
|
23
|
+
import { product } from "../product.js";
|
|
24
|
+
type Raw = Partial<Omit<StackConfig, "name">>;
|
|
25
|
+
|
|
26
|
+
function list(v: unknown, what: string): string[] {
|
|
27
|
+
if (v === undefined) return [];
|
|
28
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string"))
|
|
29
|
+
throw new Error(`${what} must be a list of strings`);
|
|
30
|
+
return v as string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toStack(
|
|
34
|
+
name: string,
|
|
35
|
+
raw: Raw | ManifestComposeConfig,
|
|
36
|
+
repo: string,
|
|
37
|
+
repoPath: string | undefined,
|
|
38
|
+
where: string,
|
|
39
|
+
): StackConfig {
|
|
40
|
+
assertStackName(name);
|
|
41
|
+
const routes = raw.routes ?? {};
|
|
42
|
+
Object.values(routes).forEach(parseTarget);
|
|
43
|
+
const files = list(raw.files, `${where}.files`);
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
repo,
|
|
47
|
+
...(repoPath ? { repoPath } : {}),
|
|
48
|
+
dir: raw.dir,
|
|
49
|
+
files: files.length ? files : ["docker-compose.yml"],
|
|
50
|
+
routes,
|
|
51
|
+
generate: list(raw.generate, `${where}.generate`),
|
|
52
|
+
uses: list(raw.uses, `${where}.uses`),
|
|
53
|
+
env: Object.fromEntries(
|
|
54
|
+
Object.entries(raw.env ?? {}).map(([k, v]) => [k, String(v)]),
|
|
55
|
+
),
|
|
56
|
+
profiles: list(raw.profiles, `${where}.profiles`),
|
|
57
|
+
...("healthchecks" in raw && raw.healthchecks
|
|
58
|
+
? { healthchecks: raw.healthchecks }
|
|
59
|
+
: {}),
|
|
60
|
+
...("service_env" in raw && raw.service_env
|
|
61
|
+
? {
|
|
62
|
+
serviceEnv: Object.fromEntries(
|
|
63
|
+
Object.entries(raw.service_env).map(([svc, vars]) => [
|
|
64
|
+
svc,
|
|
65
|
+
Object.fromEntries(
|
|
66
|
+
Object.entries(vars).map(([k, v]) => [k, String(v)]),
|
|
67
|
+
),
|
|
68
|
+
]),
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Stack name for a repo's manifest compose section. */
|
|
76
|
+
export function manifestStackName(
|
|
77
|
+
manifest: { name: string; compose?: ManifestComposeConfig },
|
|
78
|
+
repoName: string,
|
|
79
|
+
): string {
|
|
80
|
+
return manifest.compose?.name ?? manifest.name ?? repoName;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `compose:` stacks declared by repos in a workspace (default: focused). */
|
|
84
|
+
export function readManifestStacks(
|
|
85
|
+
workspace?: string,
|
|
86
|
+
): Record<string, StackConfig> {
|
|
87
|
+
const ws = workspace ? getWorkspaceByName(workspace) : getActiveWorkspace();
|
|
88
|
+
if (!ws) return {};
|
|
89
|
+
const out: Record<string, StackConfig> = {};
|
|
90
|
+
const seen = new Set<string>();
|
|
91
|
+
for (const svc of listWorkspaceServices(ws.config, readCatalog(), ws.name)) {
|
|
92
|
+
if (seen.has(svc.repoPath) || !fs.existsSync(svc.repoPath)) continue;
|
|
93
|
+
seen.add(svc.repoPath);
|
|
94
|
+
const m = readManifest(svc.repoPath);
|
|
95
|
+
if (!m?.compose) continue;
|
|
96
|
+
const name = manifestStackName(m, svc.repoName);
|
|
97
|
+
out[name] = toStack(
|
|
98
|
+
name,
|
|
99
|
+
m.compose,
|
|
100
|
+
svc.repoName,
|
|
101
|
+
svc.repoPath,
|
|
102
|
+
`${svc.repoName}/${product().manifestYml} compose`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function readStacks(
|
|
109
|
+
root: string = getDistRoot(),
|
|
110
|
+
opts: { workspace?: string; manifests?: boolean } = {},
|
|
111
|
+
): Record<string, StackConfig> {
|
|
112
|
+
let parsed: { stacks?: Record<string, Raw> } | null = null;
|
|
113
|
+
try {
|
|
114
|
+
parsed = yaml.parse(
|
|
115
|
+
fs.readFileSync(path.join(root, product().configFile), "utf-8"),
|
|
116
|
+
);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
119
|
+
}
|
|
120
|
+
const out: Record<string, StackConfig> = {};
|
|
121
|
+
for (const [name, raw] of Object.entries(parsed?.stacks ?? {})) {
|
|
122
|
+
if (!raw?.repo) throw new Error(`stacks.${name}.repo is required`);
|
|
123
|
+
out[name] = toStack(name, raw, raw.repo, undefined, `stacks.${name}`);
|
|
124
|
+
}
|
|
125
|
+
if (opts.manifests !== false)
|
|
126
|
+
Object.assign(out, readManifestStacks(opts.workspace));
|
|
127
|
+
|
|
128
|
+
// `uses` may name a stack or the repo that declares it.
|
|
129
|
+
const byRepo = new Map(Object.values(out).map((s) => [s.repo, s.name]));
|
|
130
|
+
for (const s of Object.values(out)) {
|
|
131
|
+
s.uses = s.uses.map((u) => (out[u] ? u : (byRepo.get(u) ?? u)));
|
|
132
|
+
for (const u of s.uses)
|
|
133
|
+
if (!out[u])
|
|
134
|
+
throw new Error(`stack ${s.name}: uses names unknown stack "${u}"`);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands den supplies for a repo whose `.den.yml` has a `compose:`
|
|
3
|
+
* section: the stack runs behind the edge with no host ports. A command the
|
|
4
|
+
* repo declares itself (services.<svc>.commands.local) always wins.
|
|
5
|
+
*/
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { getDistRoot } from "../paths.js";
|
|
8
|
+
import { readManifest } from "../manifest/read-manifest.js";
|
|
9
|
+
import type { RawManifest } from "../types/index.js";
|
|
10
|
+
import { manifestStackName } from "./config.js";
|
|
11
|
+
|
|
12
|
+
import { product } from "../product.js";
|
|
13
|
+
type CommandDef = string | { cmd: string; detachable?: boolean };
|
|
14
|
+
|
|
15
|
+
/** Whether `subsystem` is the repo's primary service (the one the stack belongs to). */
|
|
16
|
+
function isPrimary(manifest: RawManifest, subsystem: string): boolean {
|
|
17
|
+
const keys = Object.keys(manifest.services ?? {});
|
|
18
|
+
if (manifest.entrypoint) return manifest.entrypoint === subsystem;
|
|
19
|
+
return keys.length <= 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function denBin(): string {
|
|
23
|
+
return JSON.stringify(path.join(getDistRoot(), "bin", `${product().cli}`));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The synthesized stack commands, or {} when the repo isn't a compose stack. */
|
|
27
|
+
export function composeCommands(
|
|
28
|
+
manifest: RawManifest,
|
|
29
|
+
subsystem: string,
|
|
30
|
+
repoName: string,
|
|
31
|
+
workspace: string,
|
|
32
|
+
): Record<string, CommandDef> {
|
|
33
|
+
if (!manifest.compose || !isPrimary(manifest, subsystem)) return {};
|
|
34
|
+
// `docker compose up -d` returns once the containers are started, so these
|
|
35
|
+
// finish on their own: never daemonize them.
|
|
36
|
+
const stack = (sub: string) => ({
|
|
37
|
+
cmd: `${denBin()} stack ${sub} ${manifestStackName(manifest, repoName)} -w ${workspace}`,
|
|
38
|
+
detachable: false,
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
up: stack("up"),
|
|
42
|
+
down: stack("down"),
|
|
43
|
+
restart: stack("restart"),
|
|
44
|
+
logs: stack("logs"),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Shell health check for a compose stack. The entry service is healthy when
|
|
50
|
+
* every container is running and healthy; any other manifest service names a
|
|
51
|
+
* compose service and is checked on its own (`--service`).
|
|
52
|
+
*/
|
|
53
|
+
export function composeHealthCmd(
|
|
54
|
+
manifest: RawManifest,
|
|
55
|
+
subsystem: string,
|
|
56
|
+
repoName: string,
|
|
57
|
+
workspace: string,
|
|
58
|
+
): string | null {
|
|
59
|
+
if (!manifest.compose) return null;
|
|
60
|
+
const base = `${denBin()} stack health ${manifestStackName(manifest, repoName)} -w ${workspace}`;
|
|
61
|
+
return isPrimary(manifest, subsystem)
|
|
62
|
+
? base
|
|
63
|
+
: `${base} --service ${JSON.stringify(subsystem)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Declared local commands layered over the synthesized compose ones. */
|
|
67
|
+
export function effectiveLocalCommands(
|
|
68
|
+
manifest: RawManifest,
|
|
69
|
+
subsystem: string,
|
|
70
|
+
repoName: string,
|
|
71
|
+
workspace: string,
|
|
72
|
+
): Record<string, CommandDef> {
|
|
73
|
+
return {
|
|
74
|
+
...composeCommands(manifest, subsystem, repoName, workspace),
|
|
75
|
+
...(manifest.services?.[subsystem]?.commands?.local ?? {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Effective local commands for a resolved service, read from its manifest. */
|
|
80
|
+
export function localCommandsFor(
|
|
81
|
+
svc: { repoPath: string; repoName: string; subsystem: string },
|
|
82
|
+
workspace: string,
|
|
83
|
+
): Record<string, CommandDef> {
|
|
84
|
+
const m = readManifest(svc.repoPath);
|
|
85
|
+
return m
|
|
86
|
+
? effectiveLocalCommands(m, svc.subsystem, svc.repoName, workspace)
|
|
87
|
+
: {};
|
|
88
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure planning for stacks: project names, container names, the compose
|
|
3
|
+
* override, the link networks and the edge routes. No I/O.
|
|
4
|
+
*/
|
|
5
|
+
import * as yaml from "yaml";
|
|
6
|
+
import { edgeDomains, routeHost } from "../edge/render.js";
|
|
7
|
+
import type { EdgeConfig } from "../edge/types.js";
|
|
8
|
+
import type { ResolvedProject, StackConfig } from "./types.js";
|
|
9
|
+
|
|
10
|
+
import { product } from "../product.js";
|
|
11
|
+
const NAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
12
|
+
|
|
13
|
+
export function assertStackName(n: string): void {
|
|
14
|
+
if (!NAME_RE.test(n))
|
|
15
|
+
throw new Error(
|
|
16
|
+
`stack name "${n}" must be lowercase letters, digits and dashes`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function projectName(stack: string, workspace: string): string {
|
|
21
|
+
return `${stack}-${workspace}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Container name for a stack service. Always Compose's per-project default:
|
|
26
|
+
* the override resets any pinned `container_name`, which would otherwise
|
|
27
|
+
* collide when the same repo runs in two workspaces.
|
|
28
|
+
*/
|
|
29
|
+
export function containerName(project: string, service: string): string {
|
|
30
|
+
return `${project}-${service}-1`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function linkNetwork(
|
|
34
|
+
consumer: string,
|
|
35
|
+
provider: string,
|
|
36
|
+
ws: string,
|
|
37
|
+
): string {
|
|
38
|
+
return `${consumer}-${ws}--${provider}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function parseTarget(target: string): { service: string; port: number } {
|
|
42
|
+
const m = /^([a-zA-Z0-9][\w.-]*):(\d{1,5})$/.exec(target);
|
|
43
|
+
if (!m) throw new Error(`route target "${target}" must be <service>:<port>`);
|
|
44
|
+
return { service: m[1], port: Number(m[2]) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Expand `{stack.service}` placeholders to container names. */
|
|
48
|
+
export function expandEnv(
|
|
49
|
+
env: Record<string, string>,
|
|
50
|
+
ws: string,
|
|
51
|
+
known: Record<string, StackConfig>,
|
|
52
|
+
): Record<string, string> {
|
|
53
|
+
const out: Record<string, string> = {};
|
|
54
|
+
for (const [k, v] of Object.entries(env))
|
|
55
|
+
out[k] = v.replace(
|
|
56
|
+
/\{([a-z0-9-]+)\.([\w.-]+)\}/g,
|
|
57
|
+
(_m, s: string, svc: string) => {
|
|
58
|
+
if (!known[s]) throw new Error(`env ${k} names unknown stack "${s}"`);
|
|
59
|
+
return containerName(projectName(s, ws), svc);
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Providers named in `{stack.service}` placeholders → their services. */
|
|
66
|
+
export function providerServices(stack: StackConfig): Record<string, string[]> {
|
|
67
|
+
const out: Record<string, string[]> = {};
|
|
68
|
+
for (const v of Object.values(stack.env))
|
|
69
|
+
for (const m of v.matchAll(/\{([a-z0-9-]+)\.([\w.-]+)\}/g))
|
|
70
|
+
(out[m[1]] ??= []).includes(m[2]) || out[m[1]].push(m[2]);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Start order: every stack after the stacks it uses. Throws on cycles. */
|
|
75
|
+
export function startOrder(
|
|
76
|
+
names: string[],
|
|
77
|
+
all: Record<string, StackConfig>,
|
|
78
|
+
): string[] {
|
|
79
|
+
const order: string[] = [];
|
|
80
|
+
const state = new Map<string, "visiting" | "done">();
|
|
81
|
+
const visit = (n: string, trail: string[]) => {
|
|
82
|
+
if (!all[n]) throw new Error(`unknown stack "${n}"`);
|
|
83
|
+
if (state.get(n) === "done") return;
|
|
84
|
+
if (state.get(n) === "visiting")
|
|
85
|
+
throw new Error(
|
|
86
|
+
`stacks use each other in a cycle: ${[...trail, n].join(" → ")}`,
|
|
87
|
+
);
|
|
88
|
+
state.set(n, "visiting");
|
|
89
|
+
for (const u of all[n].uses) visit(u, [...trail, n]);
|
|
90
|
+
state.set(n, "done");
|
|
91
|
+
order.push(n);
|
|
92
|
+
};
|
|
93
|
+
names.forEach((n) => visit(n, []));
|
|
94
|
+
return order;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Services that refer to a provider: their resolved environment names one of
|
|
99
|
+
* the provider's container names. Only these join the link network, so a
|
|
100
|
+
* consumer's postgres/redis never become resolvable from the provider.
|
|
101
|
+
*/
|
|
102
|
+
export function linkedServices(
|
|
103
|
+
stack: StackConfig,
|
|
104
|
+
provider: string,
|
|
105
|
+
resolved: ResolvedProject,
|
|
106
|
+
ws: string,
|
|
107
|
+
): string[] {
|
|
108
|
+
const names = (providerServices(stack)[provider] ?? []).map((svc) =>
|
|
109
|
+
containerName(projectName(provider, ws), svc),
|
|
110
|
+
);
|
|
111
|
+
return Object.entries(resolved.services)
|
|
112
|
+
.filter(([, def]) =>
|
|
113
|
+
Object.values(def.environment ?? {}).some(
|
|
114
|
+
(v) => typeof v === "string" && names.some((n) => v.includes(n)),
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
.map(([svc]) => svc)
|
|
118
|
+
.sort();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The compose override: drop every published host port and pinned container
|
|
123
|
+
* name, and put the services that refer to a provider on its link network.
|
|
124
|
+
* Written as text because `!reset` is a Compose tag, not plain YAML.
|
|
125
|
+
*/
|
|
126
|
+
export function renderOverride(
|
|
127
|
+
stack: StackConfig,
|
|
128
|
+
resolved: ResolvedProject,
|
|
129
|
+
ws: string,
|
|
130
|
+
): string {
|
|
131
|
+
const links = stack.uses.map((p) => ({
|
|
132
|
+
net: linkNetwork(stack.name, p, ws),
|
|
133
|
+
services: linkedServices(stack, p, resolved, ws),
|
|
134
|
+
}));
|
|
135
|
+
const unknown = Object.keys(stack.serviceEnv ?? {}).filter(
|
|
136
|
+
(s) => !(s in resolved.services),
|
|
137
|
+
);
|
|
138
|
+
if (unknown.length)
|
|
139
|
+
throw new Error(
|
|
140
|
+
`stack "${stack.name}": service_env names "${unknown.join('", "')}", ` +
|
|
141
|
+
`which the compose project does not have (${Object.keys(resolved.services).sort().join(", ")})`,
|
|
142
|
+
);
|
|
143
|
+
const lines = [
|
|
144
|
+
`# Generated by \`${product().cli} stack up\`: no host ports (reach services via the`,
|
|
145
|
+
"# edge) and no pinned container names (each workspace is its own project).",
|
|
146
|
+
"services:",
|
|
147
|
+
];
|
|
148
|
+
for (const [svc, def] of Object.entries(resolved.services).sort()) {
|
|
149
|
+
const body: string[] = [];
|
|
150
|
+
if (def.ports && def.ports.length) body.push(" ports: !reset []");
|
|
151
|
+
if (def.container_name) body.push(" container_name: !reset null");
|
|
152
|
+
const check = stack.healthchecks?.[svc];
|
|
153
|
+
if (check)
|
|
154
|
+
body.push(" healthcheck:", ` test: ${JSON.stringify(check)}`);
|
|
155
|
+
const vars = stack.serviceEnv?.[svc];
|
|
156
|
+
if (vars && Object.keys(vars).length) {
|
|
157
|
+
// Compose merges `environment` by key across files, so these win.
|
|
158
|
+
body.push(" environment:");
|
|
159
|
+
for (const [k, v] of Object.entries(vars).sort())
|
|
160
|
+
body.push(` ${JSON.stringify(k)}: ${JSON.stringify(v)}`);
|
|
161
|
+
}
|
|
162
|
+
const mine = links
|
|
163
|
+
.filter((l) => l.services.includes(svc))
|
|
164
|
+
.map((l) => l.net);
|
|
165
|
+
if (mine.length) {
|
|
166
|
+
// Compose merges service networks by key, so listing the base ones keeps them.
|
|
167
|
+
const base = Object.keys(def.networks ?? { default: null });
|
|
168
|
+
body.push(" networks:");
|
|
169
|
+
for (const n of [...base, ...mine])
|
|
170
|
+
body.push(` ${JSON.stringify(n)}: {}`);
|
|
171
|
+
}
|
|
172
|
+
if (body.length) lines.push(` ${JSON.stringify(svc)}:`, ...body);
|
|
173
|
+
}
|
|
174
|
+
if (!lines.some((l) => /^ {2}"/.test(l))) lines.push(" {}");
|
|
175
|
+
const used = links.filter((l) => l.services.length).map((l) => l.net);
|
|
176
|
+
if (used.length) {
|
|
177
|
+
lines.push("networks:");
|
|
178
|
+
for (const n of used)
|
|
179
|
+
lines.push(
|
|
180
|
+
` ${JSON.stringify(n)}:`,
|
|
181
|
+
` name: ${JSON.stringify(n)}`,
|
|
182
|
+
" external: true",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return lines.join("\n") + "\n";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Docker networks the edge must join to reach each routed service. */
|
|
189
|
+
export function routedNetworks(
|
|
190
|
+
stack: StackConfig,
|
|
191
|
+
resolved: ResolvedProject,
|
|
192
|
+
): string[] {
|
|
193
|
+
const nets = new Set<string>();
|
|
194
|
+
for (const target of Object.values(stack.routes)) {
|
|
195
|
+
const { service } = parseTarget(target);
|
|
196
|
+
const def = resolved.services[service];
|
|
197
|
+
if (!def)
|
|
198
|
+
throw new Error(
|
|
199
|
+
`stack ${stack.name}: route target service "${service}" not in compose`,
|
|
200
|
+
);
|
|
201
|
+
const first = Object.keys(def.networks ?? { default: null })[0];
|
|
202
|
+
nets.add(resolved.networks?.[first]?.name ?? `${resolved.name}_${first}`);
|
|
203
|
+
}
|
|
204
|
+
return [...nets].sort();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Edge dynamic config for one stack: `<route>.<stack>.<domain>` → container. */
|
|
208
|
+
export function renderStackRoutes(
|
|
209
|
+
edge: EdgeConfig,
|
|
210
|
+
stack: StackConfig,
|
|
211
|
+
resolved: ResolvedProject,
|
|
212
|
+
): string {
|
|
213
|
+
const routers: Record<string, unknown> = {};
|
|
214
|
+
const services: Record<string, unknown> = {};
|
|
215
|
+
for (const [route, target] of Object.entries(stack.routes).sort()) {
|
|
216
|
+
if (!NAME_RE.test(route))
|
|
217
|
+
throw new Error(
|
|
218
|
+
`route "${route}" must be lowercase letters, digits and dashes`,
|
|
219
|
+
);
|
|
220
|
+
const { service, port } = parseTarget(target);
|
|
221
|
+
const key = `stack-${stack.name}-${route}`;
|
|
222
|
+
routers[key] = {
|
|
223
|
+
entryPoints: ["websecure"],
|
|
224
|
+
rule: edgeDomains(edge)
|
|
225
|
+
.map((d) => `Host(\`${routeHost(`${route}.${stack.name}`, d)}\`)`)
|
|
226
|
+
.join(" || "),
|
|
227
|
+
service: key,
|
|
228
|
+
tls: {},
|
|
229
|
+
};
|
|
230
|
+
services[key] = {
|
|
231
|
+
loadBalancer: {
|
|
232
|
+
servers: [
|
|
233
|
+
{
|
|
234
|
+
url: `http://${containerName(resolved.name, service)}:${port}`,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return (
|
|
241
|
+
`# Generated by \`${product().cli} stack up ${stack.name}\`.\n` +
|
|
242
|
+
yaml.stringify({ http: { routers, services } })
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Compose-project suffix for a den workspace: its own name, except that
|
|
248
|
+
* the `default` workspace keeps the original `main` projects (and so their
|
|
249
|
+
* volumes and data).
|
|
250
|
+
*/
|
|
251
|
+
export function stackWorkspace(denWorkspace: string): string {
|
|
252
|
+
return denWorkspace === "default" ? "main" : denWorkspace;
|
|
253
|
+
}
|