@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,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `depends_on` entry normalization.
|
|
3
|
+
*
|
|
4
|
+
* A dep can be:
|
|
5
|
+
* - a string: "shop/db"
|
|
6
|
+
* - an object: { service: "shop/db", wait: true }
|
|
7
|
+
*
|
|
8
|
+
* The object form lets a service opt into blocking startup until the dep's
|
|
9
|
+
* health check is green (default behavior is the existing 5-second detach
|
|
10
|
+
* window — no change for unflagged deps).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { DependsOnEntry } from "../types/index.js";
|
|
14
|
+
|
|
15
|
+
export interface NormalizedDep {
|
|
16
|
+
service: string;
|
|
17
|
+
wait: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function depService(dep: DependsOnEntry): string {
|
|
21
|
+
return typeof dep === "string" ? dep : dep.service;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeDep(dep: DependsOnEntry): NormalizedDep {
|
|
25
|
+
if (typeof dep === "string") return { service: dep, wait: false };
|
|
26
|
+
return { service: dep.service, wait: dep.wait ?? false };
|
|
27
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .den.yml schema migration engine.
|
|
3
|
+
*
|
|
4
|
+
* Walks the migration chain from the manifest's current version up to
|
|
5
|
+
* the requested target version (default: CURRENT_SCHEMA_VERSION).
|
|
6
|
+
*
|
|
7
|
+
* Each step is a pure function: migrate_N_to_M(manifest) → manifest
|
|
8
|
+
* Adding a new version means:
|
|
9
|
+
* 1. Bump CURRENT_SCHEMA_VERSION
|
|
10
|
+
* 2. Add migrate_<old>_to_<new> to the MIGRATION_STEPS map
|
|
11
|
+
* 3. Add the new version to schema.ts KNOWN_VERSIONS + schemaByVersion
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { RawManifest, MigrationResult } from "../types/index.js";
|
|
15
|
+
import { CURRENT_SCHEMA_VERSION } from "./schema.js";
|
|
16
|
+
|
|
17
|
+
// ── Migration step registry ───────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
type MigrationFn = (manifest: RawManifest) => {
|
|
20
|
+
manifest: RawManifest;
|
|
21
|
+
change: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Migration steps keyed by "fromVersion".
|
|
26
|
+
* migrate_1_to_2 lives at key 1, migrate_2_to_3 at key 2, etc.
|
|
27
|
+
*
|
|
28
|
+
* Currently empty — v1 is the only version.
|
|
29
|
+
* When v2 is introduced, add:
|
|
30
|
+
* [1]: migrate_1_to_2,
|
|
31
|
+
*/
|
|
32
|
+
const MIGRATION_STEPS: Record<number, MigrationFn> = {
|
|
33
|
+
// Future migrations go here, e.g.:
|
|
34
|
+
// [1]: migrate_1_to_2,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Migrate a manifest to the target schema version.
|
|
41
|
+
*
|
|
42
|
+
* @param input - A parsed manifest object (or any plain object).
|
|
43
|
+
* @param toVersion - Target version. Defaults to CURRENT_SCHEMA_VERSION.
|
|
44
|
+
* @returns MigrationResult with the migrated manifest and a log of changes.
|
|
45
|
+
* @throws When toVersion is unknown / unsupported.
|
|
46
|
+
*/
|
|
47
|
+
export function migrateManifest(
|
|
48
|
+
input: Record<string, unknown> | RawManifest,
|
|
49
|
+
toVersion: number = CURRENT_SCHEMA_VERSION,
|
|
50
|
+
): MigrationResult {
|
|
51
|
+
// Validate target version
|
|
52
|
+
if (toVersion > CURRENT_SCHEMA_VERSION) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Cannot migrate to unsupported schema version ${toVersion}. ` +
|
|
55
|
+
`Latest known version is ${CURRENT_SCHEMA_VERSION}.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fromVersion =
|
|
60
|
+
typeof (input as Record<string, unknown>).version === "number"
|
|
61
|
+
? ((input as Record<string, unknown>).version as number)
|
|
62
|
+
: 1;
|
|
63
|
+
|
|
64
|
+
// Never silently downgrade: re-stamping a lower version onto newer-schema
|
|
65
|
+
// content corrupts the manifest. Fail fast, mirroring the forward guard above.
|
|
66
|
+
if (fromVersion > toVersion) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Cannot downgrade manifest from v${fromVersion} to v${toVersion}.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const changes: string[] = [];
|
|
73
|
+
let current: RawManifest = { ...(input as RawManifest) };
|
|
74
|
+
|
|
75
|
+
// Walk the chain: fromVersion → fromVersion+1 → … → toVersion
|
|
76
|
+
for (let v = fromVersion; v < toVersion; v++) {
|
|
77
|
+
const step = MIGRATION_STEPS[v];
|
|
78
|
+
if (!step) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`No migration step found from v${v} to v${v + 1}. ` +
|
|
81
|
+
`This is a bug — please file an issue.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const { manifest: next, change } = step(current);
|
|
85
|
+
current = next;
|
|
86
|
+
changes.push(change);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Stamp the output with the target version. RawManifest has no `version`
|
|
90
|
+
// field (absence means v1), so the stamp is applied as an extra property.
|
|
91
|
+
current = { ...current, version: toVersion } as RawManifest;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
manifest: current,
|
|
95
|
+
fromVersion,
|
|
96
|
+
toVersion,
|
|
97
|
+
changes,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { CURRENT_SCHEMA_VERSION };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest reader — supports .den.yml (preferred) and .den.json (fallback).
|
|
3
|
+
*
|
|
4
|
+
* Import this helper instead of calling fs.readFileSync('.den.json') directly
|
|
5
|
+
* so all callers automatically get YAML support.
|
|
6
|
+
*
|
|
7
|
+
* A repo's own manifest always wins. When a checkout has none, Den's own
|
|
8
|
+
* copy is used: `manifests/<github-repo>.den.yml` in the Den repo, keyed by
|
|
9
|
+
* the checkout's `origin` repo name. That covers repos whose manifest is not
|
|
10
|
+
* merged yet and repos Den should not commit to.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import { execFileSync } from "child_process";
|
|
16
|
+
import { getDistRoot } from "../paths.js";
|
|
17
|
+
import { parse as parseYaml } from "yaml";
|
|
18
|
+
import type { RawManifest } from "../types/index.js";
|
|
19
|
+
|
|
20
|
+
import { product } from "../product.js";
|
|
21
|
+
/** Names probed in order. First match wins. */
|
|
22
|
+
const MANIFEST_NAMES = [
|
|
23
|
+
`${product().manifestYml}`,
|
|
24
|
+
`${product().manifestJson}`,
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
/** The repo's own manifest file, or null. */
|
|
28
|
+
export function repoManifestPath(repoPath: string): string | null {
|
|
29
|
+
for (const name of MANIFEST_NAMES) {
|
|
30
|
+
const p = path.join(repoPath, name);
|
|
31
|
+
if (fs.existsSync(p)) return p;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Den-held manifests ───────────────────────────────────────────────────
|
|
37
|
+
// For a repo that doesn't carry a manifest yet (one we don't commit to, or
|
|
38
|
+
// whose PR hasn't merged), den keeps one in manifests/<repo>.den.yml,
|
|
39
|
+
// keyed by the git origin's repo name (falling back to the folder name). The
|
|
40
|
+
// repo's own file always wins.
|
|
41
|
+
|
|
42
|
+
const originCache = new Map<string, string | null>();
|
|
43
|
+
|
|
44
|
+
/** Repo name of `repoPath`'s git origin (`…/<name>.git`), cached. */
|
|
45
|
+
export function originRepoName(repoPath: string): string | null {
|
|
46
|
+
if (originCache.has(repoPath)) return originCache.get(repoPath)!;
|
|
47
|
+
let name: string | null = null;
|
|
48
|
+
try {
|
|
49
|
+
const url = execFileSync(
|
|
50
|
+
"git",
|
|
51
|
+
["-C", repoPath, "config", "--get", "remote.origin.url"],
|
|
52
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000 },
|
|
53
|
+
).trim();
|
|
54
|
+
name =
|
|
55
|
+
url
|
|
56
|
+
.replace(/\.git$/, "")
|
|
57
|
+
.split(/[/:]/)
|
|
58
|
+
.pop() || null;
|
|
59
|
+
} catch {
|
|
60
|
+
/* not a git checkout, or no origin */
|
|
61
|
+
}
|
|
62
|
+
originCache.set(repoPath, name);
|
|
63
|
+
return name;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Directory of den-held manifests (override: DEN_MANIFESTS_DIR). */
|
|
67
|
+
export function heldManifestsDir(): string {
|
|
68
|
+
return process.env.DEN_MANIFESTS_DIR ?? path.join(getDistRoot(), "manifests");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The repo name of a `.den-services/<repo>/<branch>` path, else null. */
|
|
72
|
+
function managedCloneRepo(repoPath: string): string | null {
|
|
73
|
+
const parent = path.dirname(repoPath);
|
|
74
|
+
return path.basename(path.dirname(parent)) === `${product().servicesDir}`
|
|
75
|
+
? path.basename(parent)
|
|
76
|
+
: null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The den-held manifest for `repoPath`, or null. */
|
|
80
|
+
export function heldManifestPath(repoPath: string): string | null {
|
|
81
|
+
const keys = [
|
|
82
|
+
originRepoName(repoPath),
|
|
83
|
+
// A managed clone lives at .den-services/<repo>/<branch>,
|
|
84
|
+
// and need not be cloned yet.
|
|
85
|
+
managedCloneRepo(repoPath),
|
|
86
|
+
path.basename(repoPath),
|
|
87
|
+
];
|
|
88
|
+
for (const key of keys) {
|
|
89
|
+
if (!key) continue;
|
|
90
|
+
const p = path.join(heldManifestsDir(), `${key}${product().manifestYml}`);
|
|
91
|
+
if (fs.existsSync(p)) return p;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the manifest for `repoPath`: the repo's own .den.yml (or
|
|
98
|
+
* .json), else the one den holds for it. Returns null if neither exists.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveManifestPath(repoPath: string): string | null {
|
|
101
|
+
return repoManifestPath(repoPath) ?? heldManifestPath(repoPath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Where `repoPath`'s manifest comes from. */
|
|
105
|
+
export function manifestSource(repoPath: string): "repo" | "held" | null {
|
|
106
|
+
if (repoManifestPath(repoPath)) return "repo";
|
|
107
|
+
return heldManifestPath(repoPath) ? "held" : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read and parse the manifest from `repoPath`.
|
|
112
|
+
* Supports both YAML and JSON.
|
|
113
|
+
* Returns null if no manifest file exists or parsing fails.
|
|
114
|
+
*/
|
|
115
|
+
export function readManifest(repoPath: string): RawManifest | null {
|
|
116
|
+
const filePath = resolveManifestPath(repoPath);
|
|
117
|
+
if (!filePath) return null;
|
|
118
|
+
try {
|
|
119
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
120
|
+
const parsed = filePath.endsWith(".yml") ? parseYaml(raw) : JSON.parse(raw);
|
|
121
|
+
if (
|
|
122
|
+
parsed === null ||
|
|
123
|
+
typeof parsed !== "object" ||
|
|
124
|
+
Array.isArray(parsed)
|
|
125
|
+
) {
|
|
126
|
+
return null; // scalar/array/empty manifest is not usable
|
|
127
|
+
}
|
|
128
|
+
return parsed as RawManifest;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Async version of readManifest.
|
|
136
|
+
*/
|
|
137
|
+
export async function readManifestAsync(
|
|
138
|
+
repoPath: string,
|
|
139
|
+
): Promise<RawManifest | null> {
|
|
140
|
+
const filePath = resolveManifestPath(repoPath);
|
|
141
|
+
if (!filePath) return null;
|
|
142
|
+
try {
|
|
143
|
+
const { readFile } = await import("fs/promises");
|
|
144
|
+
const raw = await readFile(filePath, "utf-8");
|
|
145
|
+
const parsed = filePath.endsWith(".yml") ? parseYaml(raw) : JSON.parse(raw);
|
|
146
|
+
if (
|
|
147
|
+
parsed === null ||
|
|
148
|
+
typeof parsed !== "object" ||
|
|
149
|
+
Array.isArray(parsed)
|
|
150
|
+
) {
|
|
151
|
+
return null; // scalar/array/empty manifest is not usable
|
|
152
|
+
}
|
|
153
|
+
return parsed as RawManifest;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The canonical manifest filename for display/error messages. */
|
|
160
|
+
export const MANIFEST_DISPLAY_NAME = `${product().manifestYml}`;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for .den.yml manifests.
|
|
3
|
+
*
|
|
4
|
+
* Each schema version is defined separately so migrations can validate
|
|
5
|
+
* inputs and outputs at their respective version boundaries.
|
|
6
|
+
*
|
|
7
|
+
* Current version: 1 (all existing manifests without a version field are v1)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { journeysAppSchema } from "../../pillars/journeys/app-config.js";
|
|
12
|
+
|
|
13
|
+
export const CURRENT_SCHEMA_VERSION = 1;
|
|
14
|
+
export const KNOWN_VERSIONS = [1] as const;
|
|
15
|
+
|
|
16
|
+
// ── Shared primitives ─────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
// Command definition: plain string or object with options
|
|
19
|
+
const commandDef = z.union([
|
|
20
|
+
z.string(),
|
|
21
|
+
z.object({
|
|
22
|
+
cmd: z.string(),
|
|
23
|
+
detachable: z.boolean().optional(),
|
|
24
|
+
interactive: z.boolean().optional(),
|
|
25
|
+
}),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// Health check: HTTP (with or without explicit type) or shell
|
|
29
|
+
//
|
|
30
|
+
// Valid shapes:
|
|
31
|
+
// { url: "http://..." } — implicit HTTP
|
|
32
|
+
// { type: "http", url: "http://..." } — explicit HTTP
|
|
33
|
+
// { type: "shell", cmd: "pg_isready ..." } — shell
|
|
34
|
+
const healthConfig = z.union([
|
|
35
|
+
// Shell health check — must have cmd
|
|
36
|
+
z.object({
|
|
37
|
+
type: z.literal("shell"),
|
|
38
|
+
cmd: z.string({
|
|
39
|
+
required_error: '"cmd" is required for shell health checks',
|
|
40
|
+
}),
|
|
41
|
+
label: z.string().optional(),
|
|
42
|
+
}),
|
|
43
|
+
// HTTP health check (explicit or implicit type) — must have url
|
|
44
|
+
z.object({
|
|
45
|
+
type: z.union([z.literal("http"), z.undefined()]).optional(),
|
|
46
|
+
url: z.string({
|
|
47
|
+
required_error: '"url" is required for HTTP health checks',
|
|
48
|
+
}),
|
|
49
|
+
label: z.string().optional(),
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
// `distinct: true` marks a SUBSYSTEM's log as its own listed source in the
|
|
54
|
+
// /logs view (e.g. shop/db — shop runs its own postgres, not infra-stack's).
|
|
55
|
+
// Without it, only a system's primary service log is listed there.
|
|
56
|
+
const logSource = z.union([
|
|
57
|
+
z.object({ type: z.literal("process"), distinct: z.boolean().optional() }),
|
|
58
|
+
z.object({
|
|
59
|
+
type: z.literal("shell"),
|
|
60
|
+
cmd: z.string(),
|
|
61
|
+
distinct: z.boolean().optional(),
|
|
62
|
+
}),
|
|
63
|
+
z.object({
|
|
64
|
+
type: z.literal("cloudwatch"),
|
|
65
|
+
logGroup: z.string(),
|
|
66
|
+
logStream: z.string().optional(),
|
|
67
|
+
region: z.string().optional(),
|
|
68
|
+
distinct: z.boolean().optional(),
|
|
69
|
+
}),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// ── Service management config ─────────────────────────────────────────────────
|
|
73
|
+
//
|
|
74
|
+
// Keys for commands/logs/health are environment names (local, dev, …).
|
|
75
|
+
// We accept any string key here — Zod's z.record(z.enum(…)) requires all
|
|
76
|
+
// enum keys to be present (exhaustive), which is too strict for optional env blocks.
|
|
77
|
+
|
|
78
|
+
// depends_on entry: plain string or object with a `service` field and optional
|
|
79
|
+
// `wait` flag (opts this service's startup into blocking on the dep's health
|
|
80
|
+
// check — otherwise default fire-and-forget / detach-window behavior applies).
|
|
81
|
+
const dependsOnEntry = z.union([
|
|
82
|
+
z.string(),
|
|
83
|
+
z.object({
|
|
84
|
+
service: z.string(),
|
|
85
|
+
wait: z.boolean().optional(),
|
|
86
|
+
}),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const httpUrlString = z
|
|
90
|
+
.string()
|
|
91
|
+
.regex(/^https?:\/\/\S+$/, "must be an http(s) URL");
|
|
92
|
+
|
|
93
|
+
const serviceManagementConfig = z.object({
|
|
94
|
+
depends_on: z.array(dependsOnEntry).optional(),
|
|
95
|
+
commands: z.record(z.string(), z.record(z.string(), commandDef)).optional(),
|
|
96
|
+
logs: z.record(z.string(), logSource).optional(),
|
|
97
|
+
health: z.record(z.string(), healthConfig).optional(),
|
|
98
|
+
// Open-in-new-tab: env → app URL, plus named extras. http(s) only.
|
|
99
|
+
urls: z.record(z.string(), httpUrlString).optional(),
|
|
100
|
+
links: z
|
|
101
|
+
.array(
|
|
102
|
+
z.object({
|
|
103
|
+
label: z.string().min(1),
|
|
104
|
+
urls: z.record(z.string(), httpUrlString),
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
.optional(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ── Smoke / perf / synthetics ─────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
const validAuthRoles = ["admin", "user", "beta"] as const;
|
|
113
|
+
|
|
114
|
+
const smokeServiceConfig = z.object({
|
|
115
|
+
testDir: z.string(),
|
|
116
|
+
testMatch: z.string(),
|
|
117
|
+
baseUrlKey: z.enum(["api", "web"]),
|
|
118
|
+
auth: z.array(z.enum(validAuthRoles)),
|
|
119
|
+
fixtures: z.record(z.string(), z.unknown()).optional(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const performanceServiceConfig = z.object({
|
|
123
|
+
enabled: z.boolean().optional(),
|
|
124
|
+
testDir: z.string(),
|
|
125
|
+
testMatch: z.string(),
|
|
126
|
+
baseUrlKey: z.enum(["api", "web"]),
|
|
127
|
+
service: z.string(),
|
|
128
|
+
tags: z.array(z.string()).optional(),
|
|
129
|
+
fixtures: z.record(z.string(), z.unknown()).optional(),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const syntheticsConfig = z.object({
|
|
133
|
+
testDir: z.string(),
|
|
134
|
+
testMatch: z.string(),
|
|
135
|
+
baseUrlKey: z.enum(["api", "web"]),
|
|
136
|
+
auth: z.array(z.enum(validAuthRoles)),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Journeys (agent-driven E2E) — top-level like `synthetics:`, one spec home
|
|
140
|
+
// per repo. Deliberately thinner than syntheticsConfig: no baseUrlKey (the
|
|
141
|
+
// journeys runner resolves both web and api base URLs from env config) and
|
|
142
|
+
// no auth array (each journey spec's persona.role declares its own role).
|
|
143
|
+
// `app` describes the app under test (resources, session token, web-origin
|
|
144
|
+
// API feed): see src/pillars/journeys/app-config.ts.
|
|
145
|
+
const journeysConfig = z.object({
|
|
146
|
+
testDir: z.string(),
|
|
147
|
+
testMatch: z.string(),
|
|
148
|
+
app: journeysAppSchema.optional(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Security (ZAP scanning) — top-level record keyed by service name, like
|
|
152
|
+
// `smoke:`/`performance:`. Production may only ever list the passive
|
|
153
|
+
// `baseline` mode: the pillar caps it at runtime too, but a manifest that
|
|
154
|
+
// asks for more is rejected up front so the mistake is visible at validation.
|
|
155
|
+
const securityEnvironments = [
|
|
156
|
+
"local",
|
|
157
|
+
"dev",
|
|
158
|
+
"sandbox",
|
|
159
|
+
"staging",
|
|
160
|
+
"production",
|
|
161
|
+
] as const;
|
|
162
|
+
const securityModes = ["baseline", "api", "full"] as const;
|
|
163
|
+
|
|
164
|
+
const securityServiceConfig = z
|
|
165
|
+
.object({
|
|
166
|
+
baseUrlKey: z.enum(["api", "web"]).optional(),
|
|
167
|
+
hosts: z
|
|
168
|
+
.partialRecord(
|
|
169
|
+
z.enum(securityEnvironments),
|
|
170
|
+
z.array(
|
|
171
|
+
z
|
|
172
|
+
.string()
|
|
173
|
+
.url()
|
|
174
|
+
.refine((u) => /^https?:\/\//.test(u), {
|
|
175
|
+
message: "host must be an http(s) URL",
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
.optional(),
|
|
180
|
+
openapi: z.string().min(1).optional(),
|
|
181
|
+
auth: z.array(z.enum(validAuthRoles)).optional(),
|
|
182
|
+
// Passed to ZAP's `-z` option, which splits on whitespace — a regex with
|
|
183
|
+
// a literal space would silently become two options, so reject it.
|
|
184
|
+
exclude: z
|
|
185
|
+
.array(
|
|
186
|
+
z
|
|
187
|
+
.string()
|
|
188
|
+
.min(1)
|
|
189
|
+
.refine((r) => !/\s/.test(r), {
|
|
190
|
+
message: "exclude regex must not contain whitespace (use \\s)",
|
|
191
|
+
})
|
|
192
|
+
.refine(
|
|
193
|
+
(r) => {
|
|
194
|
+
try {
|
|
195
|
+
new RegExp(r);
|
|
196
|
+
return true;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{ message: "exclude entry is not a valid regular expression" },
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
.optional(),
|
|
205
|
+
modes: z
|
|
206
|
+
.partialRecord(
|
|
207
|
+
z.enum(securityEnvironments),
|
|
208
|
+
z.array(z.enum(securityModes)),
|
|
209
|
+
)
|
|
210
|
+
.optional(),
|
|
211
|
+
})
|
|
212
|
+
.refine((c) => c.baseUrlKey !== undefined || c.hosts !== undefined, {
|
|
213
|
+
message: 'one of "baseUrlKey" or "hosts" is required',
|
|
214
|
+
})
|
|
215
|
+
.refine(
|
|
216
|
+
(c) => (c.modes?.production ?? ["baseline"]).every((m) => m === "baseline"),
|
|
217
|
+
{
|
|
218
|
+
message: 'production may only allow the passive "baseline" mode',
|
|
219
|
+
path: ["modes", "production"],
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
// What is deployed where. Per Den environment, the first source that answers
|
|
223
|
+
// wins: `version` (a JSON endpoint; `field` names the commit SHA, default
|
|
224
|
+
// "build"), then `deployment` (a GitHub environment's deployments, made by
|
|
225
|
+
// `workflow`), then the newest successful `workflow` run on main.
|
|
226
|
+
const releaseSource = z.object({
|
|
227
|
+
version: z.string().optional(),
|
|
228
|
+
field: z.string().optional(),
|
|
229
|
+
deployment: z.string().optional(),
|
|
230
|
+
});
|
|
231
|
+
const releaseConfig = z.object({
|
|
232
|
+
workflow: z.string().optional(),
|
|
233
|
+
environments: z.record(z.string(), releaseSource).optional(),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// The repo's own test suites, run from its checkout. Local only unless
|
|
237
|
+
// `environments` says otherwise; the command gets DEN_ENV and BASE_URL.
|
|
238
|
+
const testSuite = z.object({
|
|
239
|
+
cmd: z.string(),
|
|
240
|
+
cwd: z.string().optional(),
|
|
241
|
+
label: z.string().optional(),
|
|
242
|
+
environments: z.array(z.string()).optional(),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const githubManifestConfig = z.object({
|
|
246
|
+
environments: z.record(z.string(), z.string()).optional(),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Docker Compose repos run behind the edge (see src/shared/stacks/types.ts).
|
|
250
|
+
const composeConfig = z.object({
|
|
251
|
+
name: z.string().optional(),
|
|
252
|
+
dir: z.string().optional(),
|
|
253
|
+
files: z.array(z.string()).optional(),
|
|
254
|
+
routes: z.record(z.string(), z.string()).optional(),
|
|
255
|
+
generate: z.array(z.string()).optional(),
|
|
256
|
+
uses: z.array(z.string()).optional(),
|
|
257
|
+
env: z
|
|
258
|
+
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
259
|
+
.optional(),
|
|
260
|
+
profiles: z.array(z.string()).optional(),
|
|
261
|
+
healthchecks: z.record(z.string(), z.array(z.string()).min(1)).optional(),
|
|
262
|
+
// service → env vars set on that service in the override (wins by key).
|
|
263
|
+
service_env: z
|
|
264
|
+
.record(
|
|
265
|
+
z.string(),
|
|
266
|
+
z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
|
|
267
|
+
)
|
|
268
|
+
.optional(),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ── v1 manifest schema ────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
export const v1ManifestSchema = z.object({
|
|
274
|
+
// Accept any integer version — the validator handles known/unknown version
|
|
275
|
+
// warnings separately so that future versions don't fail schema validation.
|
|
276
|
+
version: z.number().int().positive().optional(),
|
|
277
|
+
name: z.string({ required_error: "name is required" }),
|
|
278
|
+
entrypoint: z.string().optional(),
|
|
279
|
+
services: z.record(z.string(), serviceManagementConfig).optional(),
|
|
280
|
+
smoke: z.record(z.string(), smokeServiceConfig).optional(),
|
|
281
|
+
performance: z.record(z.string(), performanceServiceConfig).optional(),
|
|
282
|
+
synthetics: syntheticsConfig.optional(),
|
|
283
|
+
journeys: journeysConfig.optional(),
|
|
284
|
+
security: z.record(z.string(), securityServiceConfig).optional(),
|
|
285
|
+
github: githubManifestConfig.optional(),
|
|
286
|
+
compose: composeConfig.optional(),
|
|
287
|
+
application: z.string().optional(),
|
|
288
|
+
kind: z.enum(["service", "module", "library"]).optional(),
|
|
289
|
+
contributes_to: z.array(z.string()).optional(),
|
|
290
|
+
release: releaseConfig.optional(),
|
|
291
|
+
tests: z.record(z.string(), testSuite).optional(),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
export type V1Manifest = z.infer<typeof v1ManifestSchema>;
|
|
295
|
+
|
|
296
|
+
/** Map from version number to its Zod schema. */
|
|
297
|
+
export const schemaByVersion: Record<number, z.ZodTypeAny> = {
|
|
298
|
+
1: v1ManifestSchema,
|
|
299
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a person opens a service, and which org a repo belongs to.
|
|
3
|
+
*
|
|
4
|
+
* A service's open URL for an environment is its manifest `urls.<env>`, else
|
|
5
|
+
* the origin of that environment's HTTP health URL (health checks point at
|
|
6
|
+
* /readyz or /api/health, never the app, so only the origin is kept). Shell
|
|
7
|
+
* health checks and services with no HTTP check have no URL.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
Environment,
|
|
12
|
+
ServiceCatalog,
|
|
13
|
+
ServiceManagementConfig,
|
|
14
|
+
} from "../types/index.js";
|
|
15
|
+
|
|
16
|
+
export interface OpenLink {
|
|
17
|
+
label: string;
|
|
18
|
+
url: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function httpOrigin(raw: string | undefined): string | null {
|
|
22
|
+
if (!raw) return null;
|
|
23
|
+
try {
|
|
24
|
+
const u = new URL(raw);
|
|
25
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") return null;
|
|
26
|
+
return `${u.origin}/`;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function httpUrl(raw: string | undefined): string | null {
|
|
33
|
+
if (!raw) return null;
|
|
34
|
+
try {
|
|
35
|
+
const u = new URL(raw);
|
|
36
|
+
return u.protocol === "https:" || u.protocol === "http:" ? u.href : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function openUrlFor(
|
|
43
|
+
svc: ServiceManagementConfig | undefined,
|
|
44
|
+
env: Environment,
|
|
45
|
+
): string | null {
|
|
46
|
+
if (!svc) return null;
|
|
47
|
+
const declared = httpUrl(svc.urls?.[env]);
|
|
48
|
+
if (declared) return declared;
|
|
49
|
+
const health = svc.health?.[env];
|
|
50
|
+
if (!health || ("type" in health && health.type === "shell")) return null;
|
|
51
|
+
return httpOrigin((health as { url?: string }).url);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function linksFor(
|
|
55
|
+
svc: ServiceManagementConfig | undefined,
|
|
56
|
+
env: Environment,
|
|
57
|
+
): OpenLink[] {
|
|
58
|
+
const out: OpenLink[] = [];
|
|
59
|
+
for (const l of svc?.links ?? []) {
|
|
60
|
+
const url = httpUrl(l.urls?.[env]);
|
|
61
|
+
if (url && l.label) out.push({ label: l.label, url });
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The GitHub owner a repo is catalogued under ("acme"), or "". */
|
|
67
|
+
export function orgOf(repo: string, catalog: ServiceCatalog): string {
|
|
68
|
+
const gh = catalog[repo]?.gh ?? "";
|
|
69
|
+
const slash = gh.indexOf("/");
|
|
70
|
+
return slash > 0 ? gh.slice(0, slash) : "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface OrgDisplay {
|
|
74
|
+
label?: string;
|
|
75
|
+
color?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Every org in the catalog with its display name, configured ones first. */
|
|
79
|
+
export function listOrgs(
|
|
80
|
+
catalog: ServiceCatalog,
|
|
81
|
+
configured: Record<string, OrgDisplay> = {},
|
|
82
|
+
): Array<{ id: string; label: string; color?: string }> {
|
|
83
|
+
const ids = new Set<string>(Object.keys(configured));
|
|
84
|
+
for (const repo of Object.keys(catalog)) {
|
|
85
|
+
const o = orgOf(repo, catalog);
|
|
86
|
+
if (o) ids.add(o);
|
|
87
|
+
}
|
|
88
|
+
return [...ids].map((id) => ({
|
|
89
|
+
id,
|
|
90
|
+
label: configured[id]?.label ?? id,
|
|
91
|
+
...(configured[id]?.color ? { color: configured[id].color } : {}),
|
|
92
|
+
}));
|
|
93
|
+
}
|