@monster-orange/den 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +166 -2
- package/bin/den +61 -0
- package/docs/distributions.md +256 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +104 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +406 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +566 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +178 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/workspace-routes.ts +2154 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace pill / picker models.
|
|
3
|
+
*
|
|
4
|
+
* Pure and DOM-free (like perf-report.js) so the shape of the header's
|
|
5
|
+
* workspace menu is unit-testable under Jest's node environment; header.js
|
|
6
|
+
* owns everything that touches the DOM.
|
|
7
|
+
*
|
|
8
|
+
* Input is always the `/api/workspaces` payload:
|
|
9
|
+
* { workspaces: { [name]: config }, active: string[] | string, focus?: string }
|
|
10
|
+
* `active` is an array since multi-active workspaces; older payloads sent a
|
|
11
|
+
* single string, which both helpers still accept.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Active workspace names, normalised to an array. */
|
|
15
|
+
function activeNames(data) {
|
|
16
|
+
const active = data?.active;
|
|
17
|
+
if (Array.isArray(active)) return active.filter(Boolean);
|
|
18
|
+
return active ? [active] : [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Focused workspace name: explicit focus → first active → null. */
|
|
22
|
+
function focusName(data) {
|
|
23
|
+
const active = activeNames(data);
|
|
24
|
+
if (data?.focus) return data.focus;
|
|
25
|
+
return active[0] ?? null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Label + tooltip for the header pill.
|
|
30
|
+
* Falls back to "default" so the pill never renders empty.
|
|
31
|
+
*/
|
|
32
|
+
export function buildWorkspacePillModel(data) {
|
|
33
|
+
const active = activeNames(data);
|
|
34
|
+
const label = focusName(data) ?? "default";
|
|
35
|
+
return {
|
|
36
|
+
label,
|
|
37
|
+
// Only worth spelling out when more than one workspace is active —
|
|
38
|
+
// otherwise the pill text already says everything.
|
|
39
|
+
title:
|
|
40
|
+
active.length > 1
|
|
41
|
+
? `Active: ${active.join(", ")} · Focus: ${label}`
|
|
42
|
+
: "Switch workspace",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Menu model for the pill's dropdown: every known workspace, active ones
|
|
48
|
+
* first (they are the likely targets), each side alphabetical so the list
|
|
49
|
+
* order is stable across polls.
|
|
50
|
+
*/
|
|
51
|
+
export function buildWorkspaceMenuModel(data) {
|
|
52
|
+
const active = new Set(activeNames(data));
|
|
53
|
+
const focus = focusName(data);
|
|
54
|
+
// Union of the catalog and the active set: a workspace listed as active but
|
|
55
|
+
// missing from `workspaces` still has to be selectable (and visible).
|
|
56
|
+
const names = new Set([...Object.keys(data?.workspaces ?? {}), ...active]);
|
|
57
|
+
|
|
58
|
+
const items = [...names].map((name) => ({
|
|
59
|
+
name,
|
|
60
|
+
active: active.has(name),
|
|
61
|
+
focused: name === focus,
|
|
62
|
+
}));
|
|
63
|
+
items.sort(
|
|
64
|
+
(a, b) =>
|
|
65
|
+
Number(b.active) - Number(a.active) || a.name.localeCompare(b.name),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return { focus, items };
|
|
69
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy session manager for the Release tab.
|
|
3
|
+
*
|
|
4
|
+
* A deploy is a long-running, server-side thing (dispatch → gate → monitor) that
|
|
5
|
+
* MUST outlive any single browser page. Den's tabs are separate full-page
|
|
6
|
+
* navigations, so if the deploy were tied to the SSE request (as the first cut
|
|
7
|
+
* was), navigating away from Release would both lose the log AND abort den's
|
|
8
|
+
* monitoring of a real, already-dispatched deploy.
|
|
9
|
+
*
|
|
10
|
+
* This decouples them: `start()` runs the deploy in the background against an
|
|
11
|
+
* in-memory replay buffer; browser clients `attach()` to REPLAY the log so far
|
|
12
|
+
* and then stream live events. Re-attaching after a reload restores the full
|
|
13
|
+
* log. The deploy runs to completion regardless of who is (or isn't) attached;
|
|
14
|
+
* only an explicit `dismiss()` (the user closing the log) stops it.
|
|
15
|
+
*
|
|
16
|
+
* Sessions run CONCURRENTLY. Deploys of different systems are independent —
|
|
17
|
+
* they dispatch to different repos and GitHub scopes its own `concurrency:`
|
|
18
|
+
* group per repo-and-environment — so a global lock only stopped the operator
|
|
19
|
+
* from doing things that were never in conflict. The one real collision is
|
|
20
|
+
* dispatching the SAME repo to the SAME environment twice while the first is
|
|
21
|
+
* still in flight, which `conflicts()` rejects.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
releaseService,
|
|
26
|
+
type DeployPlanEntry,
|
|
27
|
+
type ReleaseEvent,
|
|
28
|
+
} from "./release-service.js";
|
|
29
|
+
import {
|
|
30
|
+
recordInvocation,
|
|
31
|
+
recordFailure,
|
|
32
|
+
} from "../../shared/dev-metrics/emit.js";
|
|
33
|
+
|
|
34
|
+
/** Public (serializable) view of a session — no subscribers/internal flags. */
|
|
35
|
+
export interface DeploySessionInfo {
|
|
36
|
+
id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
env: string;
|
|
39
|
+
mode: string;
|
|
40
|
+
status: "running" | "done";
|
|
41
|
+
ok: boolean | null;
|
|
42
|
+
startedAt: number;
|
|
43
|
+
endedAt: number | null;
|
|
44
|
+
/** Repos this session deploys — lets the UI disable just their buttons. */
|
|
45
|
+
repos: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type Subscriber = (ev: ReleaseEvent) => void;
|
|
49
|
+
|
|
50
|
+
interface Session extends DeploySessionInfo {
|
|
51
|
+
events: ReleaseEvent[];
|
|
52
|
+
subs: Set<Subscriber>;
|
|
53
|
+
dismissed: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A deploy log is modest (dispatch/gate/monitor lines); cap the replay buffer so
|
|
57
|
+
// a pathologically long-running deploy can't grow it without bound.
|
|
58
|
+
const MAX_EVENTS = 5000;
|
|
59
|
+
|
|
60
|
+
// Finished sessions are retained so a reload can restore their log. Cap how
|
|
61
|
+
// many, so a long session of many deploys can't grow the map without bound.
|
|
62
|
+
const MAX_FINISHED_SESSIONS = 20;
|
|
63
|
+
|
|
64
|
+
function newId(): string {
|
|
65
|
+
return `dep_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class DeploySessionManager {
|
|
69
|
+
private sessions = new Map<string, Session>();
|
|
70
|
+
|
|
71
|
+
private info(s: Session): DeploySessionInfo {
|
|
72
|
+
const { events: _e, subs: _s, dismissed: _d, ...info } = s;
|
|
73
|
+
void _e;
|
|
74
|
+
void _s;
|
|
75
|
+
void _d;
|
|
76
|
+
return info;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** All restorable sessions (running or finished), newest first. */
|
|
80
|
+
active(): DeploySessionInfo[] {
|
|
81
|
+
return [...this.sessions.values()]
|
|
82
|
+
.filter((s) => !s.dismissed)
|
|
83
|
+
.sort((a, b) => b.startedAt - a.startedAt)
|
|
84
|
+
.map((s) => this.info(s));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** True while ANY deploy is live. Kept for callers that just need a flag. */
|
|
88
|
+
busy(): boolean {
|
|
89
|
+
return [...this.sessions.values()].some(
|
|
90
|
+
(s) => !s.dismissed && s.status === "running",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Repos in `plan` that already have a live deploy to `env` — dispatching the
|
|
96
|
+
* same repo+env twice would race two runs against one GitHub concurrency
|
|
97
|
+
* group. Different repos, or the same repo to a different env, do not
|
|
98
|
+
* conflict. Empty array means the deploy is safe to start.
|
|
99
|
+
*/
|
|
100
|
+
conflicts(env: string, plan: DeployPlanEntry[]): string[] {
|
|
101
|
+
const live = new Set<string>();
|
|
102
|
+
for (const s of this.sessions.values()) {
|
|
103
|
+
if (s.dismissed || s.status !== "running" || s.env !== env) continue;
|
|
104
|
+
for (const r of s.repos) live.add(r);
|
|
105
|
+
}
|
|
106
|
+
return plan.map((p) => p.repo).filter((r) => live.has(r));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Start a deploy in the background. `notices` are emitted into the log before
|
|
111
|
+
* the deploy begins (e.g. the header line and a hotfix warning). Callers
|
|
112
|
+
* should check `conflicts()` first — this does not re-check.
|
|
113
|
+
*/
|
|
114
|
+
start(opts: {
|
|
115
|
+
env: string;
|
|
116
|
+
mode: string;
|
|
117
|
+
label: string;
|
|
118
|
+
plan: DeployPlanEntry[];
|
|
119
|
+
notices?: ReleaseEvent[];
|
|
120
|
+
}): DeploySessionInfo {
|
|
121
|
+
const session: Session = {
|
|
122
|
+
id: newId(),
|
|
123
|
+
label: opts.label,
|
|
124
|
+
env: opts.env,
|
|
125
|
+
mode: opts.mode,
|
|
126
|
+
status: "running",
|
|
127
|
+
ok: null,
|
|
128
|
+
startedAt: Date.now(),
|
|
129
|
+
endedAt: null,
|
|
130
|
+
repos: opts.plan.map((p) => p.repo),
|
|
131
|
+
events: [],
|
|
132
|
+
subs: new Set(),
|
|
133
|
+
dismissed: false,
|
|
134
|
+
};
|
|
135
|
+
this.sessions.set(session.id, session);
|
|
136
|
+
this.prune();
|
|
137
|
+
|
|
138
|
+
const emit = (ev: ReleaseEvent): void => {
|
|
139
|
+
session.events.push(ev);
|
|
140
|
+
if (session.events.length > MAX_EVENTS) {
|
|
141
|
+
session.events.splice(0, session.events.length - MAX_EVENTS);
|
|
142
|
+
}
|
|
143
|
+
for (const sub of session.subs) {
|
|
144
|
+
try {
|
|
145
|
+
sub(ev);
|
|
146
|
+
} catch {
|
|
147
|
+
/* a dead subscriber must not break the fan-out */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
for (const n of opts.notices ?? []) emit(n);
|
|
153
|
+
|
|
154
|
+
const startedAt = session.startedAt;
|
|
155
|
+
// Decoupled from any client. aborted() fires only when the user dismisses
|
|
156
|
+
// the log, giving an escape hatch for a stuck monitor; navigation does not.
|
|
157
|
+
void releaseService
|
|
158
|
+
.deploy(opts.env, opts.plan, emit, () => session.dismissed)
|
|
159
|
+
.then((ok) => {
|
|
160
|
+
session.ok = ok;
|
|
161
|
+
recordInvocation({
|
|
162
|
+
command: "release.deploy",
|
|
163
|
+
surface: "ux",
|
|
164
|
+
durationMs: Date.now() - startedAt,
|
|
165
|
+
exit: ok ? 0 : 1,
|
|
166
|
+
extra: { mode: opts.mode, env: opts.env, repos: opts.plan.length },
|
|
167
|
+
});
|
|
168
|
+
})
|
|
169
|
+
.catch((e) => {
|
|
170
|
+
session.ok = false;
|
|
171
|
+
recordFailure({
|
|
172
|
+
command: "release.deploy",
|
|
173
|
+
surface: "ux",
|
|
174
|
+
reason:
|
|
175
|
+
(e as { constructor?: { name?: string } })?.constructor?.name ??
|
|
176
|
+
"Error",
|
|
177
|
+
});
|
|
178
|
+
emit({
|
|
179
|
+
type: "log",
|
|
180
|
+
level: "error",
|
|
181
|
+
text: `Deploy error: ${(e as Error).message}`,
|
|
182
|
+
});
|
|
183
|
+
emit({ type: "done", ok: false, summary: "Deploy error." });
|
|
184
|
+
})
|
|
185
|
+
.finally(() => {
|
|
186
|
+
session.status = "done";
|
|
187
|
+
session.endedAt = Date.now();
|
|
188
|
+
// Dismissed mid-run: nothing can attach to it now, so drop it once the
|
|
189
|
+
// deploy loop has actually exited.
|
|
190
|
+
if (session.dismissed) this.sessions.delete(session.id);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return this.info(session);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Bound the map. Finished sessions are kept so a reload can still restore the
|
|
198
|
+
* log, but not forever — drop the oldest finished ones past the cap. Running
|
|
199
|
+
* sessions are never pruned.
|
|
200
|
+
*/
|
|
201
|
+
private prune(): void {
|
|
202
|
+
const finished = [...this.sessions.values()]
|
|
203
|
+
.filter((s) => s.status === "done")
|
|
204
|
+
.sort((a, b) => (a.endedAt ?? 0) - (b.endedAt ?? 0));
|
|
205
|
+
const excess = finished.length - MAX_FINISHED_SESSIONS;
|
|
206
|
+
for (let i = 0; i < excess; i++) this.sessions.delete(finished[i].id);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Attach a subscriber to session `id`: replays every buffered event (the full
|
|
211
|
+
* log, terminal `done` included when finished) then streams live ones.
|
|
212
|
+
* Returns an unsubscribe fn, or null if the session is gone/dismissed.
|
|
213
|
+
*/
|
|
214
|
+
attach(id: string, sub: Subscriber): (() => void) | null {
|
|
215
|
+
const s = this.sessions.get(id);
|
|
216
|
+
if (!s || s.dismissed) return null;
|
|
217
|
+
for (const ev of s.events) {
|
|
218
|
+
try {
|
|
219
|
+
sub(ev);
|
|
220
|
+
} catch {
|
|
221
|
+
/* ignore */
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
s.subs.add(sub);
|
|
225
|
+
return () => {
|
|
226
|
+
s.subs.delete(sub);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Dismiss (close) session `id`: signals the deploy to stop monitoring and
|
|
232
|
+
* removes it from restore. A finished session's slot is freed immediately; a
|
|
233
|
+
* running one frees once its loop exits.
|
|
234
|
+
*/
|
|
235
|
+
dismiss(id: string): boolean {
|
|
236
|
+
const s = this.sessions.get(id);
|
|
237
|
+
if (!s) return false;
|
|
238
|
+
s.dismissed = true;
|
|
239
|
+
// A finished session can go now; a running one is dropped by its own
|
|
240
|
+
// .finally() once the deploy loop notices the dismissal and exits.
|
|
241
|
+
if (s.status === "done") this.sessions.delete(id);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export const deploySessions = new DeploySessionManager();
|