tlc-claude-code 1.4.8 → 1.4.9
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/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Executor Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
createDeployment,
|
|
7
|
+
executeDeployment,
|
|
8
|
+
runHealthChecks,
|
|
9
|
+
switchTraffic,
|
|
10
|
+
cleanupOldDeployment,
|
|
11
|
+
DEPLOYMENT_STATES,
|
|
12
|
+
DEPLOYMENT_STRATEGIES,
|
|
13
|
+
createDeploymentExecutor,
|
|
14
|
+
} from './deployment-executor.js';
|
|
15
|
+
|
|
16
|
+
describe('deployment-executor', () => {
|
|
17
|
+
describe('DEPLOYMENT_STATES', () => {
|
|
18
|
+
it('defines all state constants', () => {
|
|
19
|
+
expect(DEPLOYMENT_STATES.PENDING).toBe('pending');
|
|
20
|
+
expect(DEPLOYMENT_STATES.BUILDING).toBe('building');
|
|
21
|
+
expect(DEPLOYMENT_STATES.DEPLOYING).toBe('deploying');
|
|
22
|
+
expect(DEPLOYMENT_STATES.HEALTH_CHECK).toBe('health_check');
|
|
23
|
+
expect(DEPLOYMENT_STATES.SWITCHING).toBe('switching');
|
|
24
|
+
expect(DEPLOYMENT_STATES.COMPLETED).toBe('completed');
|
|
25
|
+
expect(DEPLOYMENT_STATES.FAILED).toBe('failed');
|
|
26
|
+
expect(DEPLOYMENT_STATES.ROLLED_BACK).toBe('rolled_back');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('DEPLOYMENT_STRATEGIES', () => {
|
|
31
|
+
it('defines all strategy constants', () => {
|
|
32
|
+
expect(DEPLOYMENT_STRATEGIES.ROLLING).toBe('rolling');
|
|
33
|
+
expect(DEPLOYMENT_STRATEGIES.BLUE_GREEN).toBe('blue-green');
|
|
34
|
+
expect(DEPLOYMENT_STRATEGIES.CANARY).toBe('canary');
|
|
35
|
+
expect(DEPLOYMENT_STRATEGIES.RECREATE).toBe('recreate');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('createDeployment', () => {
|
|
40
|
+
it('creates deployment with required fields', () => {
|
|
41
|
+
const deployment = createDeployment({
|
|
42
|
+
branch: 'main',
|
|
43
|
+
commitSha: 'abc123',
|
|
44
|
+
strategy: 'blue-green',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(deployment.id).toBeDefined();
|
|
48
|
+
expect(deployment.branch).toBe('main');
|
|
49
|
+
expect(deployment.commitSha).toBe('abc123');
|
|
50
|
+
expect(deployment.strategy).toBe('blue-green');
|
|
51
|
+
expect(deployment.state).toBe('pending');
|
|
52
|
+
expect(deployment.createdAt).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('defaults to rolling strategy', () => {
|
|
56
|
+
const deployment = createDeployment({
|
|
57
|
+
branch: 'feature/x',
|
|
58
|
+
commitSha: 'abc123',
|
|
59
|
+
});
|
|
60
|
+
expect(deployment.strategy).toBe('rolling');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('tracks state transitions', () => {
|
|
64
|
+
const deployment = createDeployment({
|
|
65
|
+
branch: 'main',
|
|
66
|
+
commitSha: 'abc123',
|
|
67
|
+
});
|
|
68
|
+
expect(deployment.stateHistory).toEqual([
|
|
69
|
+
expect.objectContaining({ state: 'pending' }),
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('executeDeployment', () => {
|
|
75
|
+
it('executes rolling deployment', async () => {
|
|
76
|
+
const mockBuild = vi.fn().mockResolvedValue({ success: true });
|
|
77
|
+
const mockDeploy = vi.fn().mockResolvedValue({ success: true });
|
|
78
|
+
const mockHealth = vi.fn().mockResolvedValue({ healthy: true });
|
|
79
|
+
|
|
80
|
+
const deployment = createDeployment({
|
|
81
|
+
branch: 'feature/x',
|
|
82
|
+
commitSha: 'abc123',
|
|
83
|
+
strategy: 'rolling',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const result = await executeDeployment(deployment, {
|
|
87
|
+
build: mockBuild,
|
|
88
|
+
deploy: mockDeploy,
|
|
89
|
+
healthCheck: mockHealth,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result.state).toBe('completed');
|
|
93
|
+
expect(mockBuild).toHaveBeenCalled();
|
|
94
|
+
expect(mockDeploy).toHaveBeenCalled();
|
|
95
|
+
expect(mockHealth).toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('executes blue-green deployment', async () => {
|
|
99
|
+
const mockBuild = vi.fn().mockResolvedValue({ success: true });
|
|
100
|
+
const mockDeploy = vi.fn().mockResolvedValue({ success: true, slot: 'green' });
|
|
101
|
+
const mockHealth = vi.fn().mockResolvedValue({ healthy: true });
|
|
102
|
+
const mockSwitch = vi.fn().mockResolvedValue({ success: true });
|
|
103
|
+
|
|
104
|
+
const deployment = createDeployment({
|
|
105
|
+
branch: 'main',
|
|
106
|
+
commitSha: 'abc123',
|
|
107
|
+
strategy: 'blue-green',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await executeDeployment(deployment, {
|
|
111
|
+
build: mockBuild,
|
|
112
|
+
deploy: mockDeploy,
|
|
113
|
+
healthCheck: mockHealth,
|
|
114
|
+
switchTraffic: mockSwitch,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(result.state).toBe('completed');
|
|
118
|
+
expect(mockSwitch).toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('fails on build error', async () => {
|
|
122
|
+
const mockBuild = vi.fn().mockResolvedValue({ success: false, error: 'Build failed' });
|
|
123
|
+
|
|
124
|
+
const deployment = createDeployment({
|
|
125
|
+
branch: 'main',
|
|
126
|
+
commitSha: 'abc123',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const result = await executeDeployment(deployment, {
|
|
130
|
+
build: mockBuild,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(result.state).toBe('failed');
|
|
134
|
+
expect(result.error).toContain('Build failed');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('fails on health check failure', async () => {
|
|
138
|
+
const mockBuild = vi.fn().mockResolvedValue({ success: true });
|
|
139
|
+
const mockDeploy = vi.fn().mockResolvedValue({ success: true });
|
|
140
|
+
const mockHealth = vi.fn().mockResolvedValue({ healthy: false, reason: 'Timeout' });
|
|
141
|
+
|
|
142
|
+
const deployment = createDeployment({
|
|
143
|
+
branch: 'main',
|
|
144
|
+
commitSha: 'abc123',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const result = await executeDeployment(deployment, {
|
|
148
|
+
build: mockBuild,
|
|
149
|
+
deploy: mockDeploy,
|
|
150
|
+
healthCheck: mockHealth,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(result.state).toBe('failed');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('emits state change events', async () => {
|
|
157
|
+
const mockBuild = vi.fn().mockResolvedValue({ success: true });
|
|
158
|
+
const mockDeploy = vi.fn().mockResolvedValue({ success: true });
|
|
159
|
+
const mockHealth = vi.fn().mockResolvedValue({ healthy: true });
|
|
160
|
+
const onStateChange = vi.fn();
|
|
161
|
+
|
|
162
|
+
const deployment = createDeployment({
|
|
163
|
+
branch: 'main',
|
|
164
|
+
commitSha: 'abc123',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await executeDeployment(deployment, {
|
|
168
|
+
build: mockBuild,
|
|
169
|
+
deploy: mockDeploy,
|
|
170
|
+
healthCheck: mockHealth,
|
|
171
|
+
onStateChange,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(onStateChange).toHaveBeenCalledWith(expect.objectContaining({ state: 'building' }));
|
|
175
|
+
expect(onStateChange).toHaveBeenCalledWith(expect.objectContaining({ state: 'deploying' }));
|
|
176
|
+
expect(onStateChange).toHaveBeenCalledWith(expect.objectContaining({ state: 'completed' }));
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('runHealthChecks', () => {
|
|
181
|
+
it('runs multiple health check endpoints', async () => {
|
|
182
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
|
|
183
|
+
|
|
184
|
+
const result = await runHealthChecks({
|
|
185
|
+
endpoints: ['http://localhost:3000/health', 'http://localhost:3000/ready'],
|
|
186
|
+
fetch: mockFetch,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(result.healthy).toBe(true);
|
|
190
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('fails if any endpoint unhealthy', async () => {
|
|
194
|
+
const mockFetch = vi.fn()
|
|
195
|
+
.mockResolvedValueOnce({ ok: true, status: 200 })
|
|
196
|
+
.mockResolvedValueOnce({ ok: false, status: 503 });
|
|
197
|
+
|
|
198
|
+
const result = await runHealthChecks({
|
|
199
|
+
endpoints: ['http://localhost:3000/health', 'http://localhost:3000/ready'],
|
|
200
|
+
fetch: mockFetch,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(result.healthy).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('retries failed checks', async () => {
|
|
207
|
+
const mockFetch = vi.fn()
|
|
208
|
+
.mockResolvedValueOnce({ ok: false, status: 503 })
|
|
209
|
+
.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
210
|
+
|
|
211
|
+
const result = await runHealthChecks({
|
|
212
|
+
endpoints: ['http://localhost:3000/health'],
|
|
213
|
+
fetch: mockFetch,
|
|
214
|
+
retries: 2,
|
|
215
|
+
retryDelay: 10,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(result.healthy).toBe(true);
|
|
219
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('times out slow health checks', async () => {
|
|
223
|
+
const mockFetch = vi.fn().mockImplementation(() =>
|
|
224
|
+
new Promise((resolve) => setTimeout(() => resolve({ ok: true }), 1000))
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const result = await runHealthChecks({
|
|
228
|
+
endpoints: ['http://localhost:3000/health'],
|
|
229
|
+
fetch: mockFetch,
|
|
230
|
+
timeout: 50,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(result.healthy).toBe(false);
|
|
234
|
+
expect(result.reason).toContain('timeout');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('switchTraffic', () => {
|
|
239
|
+
it('switches traffic to new slot', async () => {
|
|
240
|
+
const mockSwitch = vi.fn().mockResolvedValue({ success: true });
|
|
241
|
+
|
|
242
|
+
const result = await switchTraffic({
|
|
243
|
+
fromSlot: 'blue',
|
|
244
|
+
toSlot: 'green',
|
|
245
|
+
switchFn: mockSwitch,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(result.success).toBe(true);
|
|
249
|
+
expect(mockSwitch).toHaveBeenCalledWith('blue', 'green');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('handles switch failure', async () => {
|
|
253
|
+
const mockSwitch = vi.fn().mockRejectedValue(new Error('Switch failed'));
|
|
254
|
+
|
|
255
|
+
const result = await switchTraffic({
|
|
256
|
+
fromSlot: 'blue',
|
|
257
|
+
toSlot: 'green',
|
|
258
|
+
switchFn: mockSwitch,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(result.success).toBe(false);
|
|
262
|
+
expect(result.error).toContain('Switch failed');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('cleanupOldDeployment', () => {
|
|
267
|
+
it('removes old deployment resources', async () => {
|
|
268
|
+
const mockCleanup = vi.fn().mockResolvedValue({ success: true });
|
|
269
|
+
|
|
270
|
+
const result = await cleanupOldDeployment({
|
|
271
|
+
slot: 'blue',
|
|
272
|
+
cleanupFn: mockCleanup,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(result.success).toBe(true);
|
|
276
|
+
expect(mockCleanup).toHaveBeenCalledWith('blue');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('handles cleanup failure gracefully', async () => {
|
|
280
|
+
const mockCleanup = vi.fn().mockRejectedValue(new Error('Cleanup failed'));
|
|
281
|
+
|
|
282
|
+
const result = await cleanupOldDeployment({
|
|
283
|
+
slot: 'blue',
|
|
284
|
+
cleanupFn: mockCleanup,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Should not throw, just log warning
|
|
288
|
+
expect(result.success).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('createDeploymentExecutor', () => {
|
|
293
|
+
it('creates executor with config', () => {
|
|
294
|
+
const executor = createDeploymentExecutor();
|
|
295
|
+
expect(executor.execute).toBeDefined();
|
|
296
|
+
expect(executor.getDeployment).toBeDefined();
|
|
297
|
+
expect(executor.cancel).toBeDefined();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('tracks active deployments', async () => {
|
|
301
|
+
const executor = createDeploymentExecutor({
|
|
302
|
+
build: vi.fn().mockResolvedValue({ success: true }),
|
|
303
|
+
deploy: vi.fn().mockResolvedValue({ success: true }),
|
|
304
|
+
healthCheck: vi.fn().mockResolvedValue({ healthy: true }),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const deployment = await executor.start({
|
|
308
|
+
branch: 'main',
|
|
309
|
+
commitSha: 'abc123',
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
expect(executor.getDeployment(deployment.id)).toBeDefined();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('lists all deployments', async () => {
|
|
316
|
+
const executor = createDeploymentExecutor({
|
|
317
|
+
build: vi.fn().mockResolvedValue({ success: true }),
|
|
318
|
+
deploy: vi.fn().mockResolvedValue({ success: true }),
|
|
319
|
+
healthCheck: vi.fn().mockResolvedValue({ healthy: true }),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
await executor.start({ branch: 'main', commitSha: 'abc123' });
|
|
323
|
+
await executor.start({ branch: 'dev', commitSha: 'def456' });
|
|
324
|
+
|
|
325
|
+
const deployments = executor.list();
|
|
326
|
+
expect(deployments).toHaveLength(2);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Rules
|
|
3
|
+
*
|
|
4
|
+
* Defines and manages deployment rules for different tiers (feature, dev, stable).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default deployment rules for each tier
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_RULES = {
|
|
11
|
+
feature: {
|
|
12
|
+
autoDeploy: true,
|
|
13
|
+
securityGates: ['sast', 'dependencies'],
|
|
14
|
+
deploymentStrategy: 'rolling',
|
|
15
|
+
requiresApproval: false,
|
|
16
|
+
requires2FA: false,
|
|
17
|
+
},
|
|
18
|
+
dev: {
|
|
19
|
+
autoDeploy: true,
|
|
20
|
+
securityGates: ['sast', 'dast', 'container', 'dependencies'],
|
|
21
|
+
deploymentStrategy: 'rolling',
|
|
22
|
+
requiresApproval: false,
|
|
23
|
+
requires2FA: false,
|
|
24
|
+
},
|
|
25
|
+
stable: {
|
|
26
|
+
autoDeploy: false,
|
|
27
|
+
securityGates: ['sast', 'dast', 'container', 'dependencies', 'pentest'],
|
|
28
|
+
deploymentStrategy: 'blue-green',
|
|
29
|
+
requiresApproval: true,
|
|
30
|
+
requires2FA: true,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Valid deployment strategies
|
|
36
|
+
*/
|
|
37
|
+
const VALID_STRATEGIES = ['rolling', 'blue-green', 'canary', 'recreate'];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load deployment rules from config object
|
|
41
|
+
* @param {object} config - Configuration object with optional deployment.rules
|
|
42
|
+
* @returns {object} Merged rules with defaults
|
|
43
|
+
*/
|
|
44
|
+
export function loadDeploymentRules(config) {
|
|
45
|
+
if (!config || !config.deployment || !config.deployment.rules) {
|
|
46
|
+
return DEFAULT_RULES;
|
|
47
|
+
}
|
|
48
|
+
return mergeWithDefaults(config.deployment.rules);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate rules schema
|
|
53
|
+
* @param {object} rules - Rules object to validate
|
|
54
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
55
|
+
*/
|
|
56
|
+
export function validateRules(rules) {
|
|
57
|
+
const errors = [];
|
|
58
|
+
|
|
59
|
+
for (const [tier, tierRules] of Object.entries(rules)) {
|
|
60
|
+
if (tierRules.autoDeploy !== undefined && typeof tierRules.autoDeploy !== 'boolean') {
|
|
61
|
+
errors.push(`${tier}.autoDeploy must be a boolean`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (tierRules.securityGates !== undefined && !Array.isArray(tierRules.securityGates)) {
|
|
65
|
+
errors.push(`${tier}.securityGates must be an array`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
tierRules.deploymentStrategy !== undefined &&
|
|
70
|
+
!VALID_STRATEGIES.includes(tierRules.deploymentStrategy)
|
|
71
|
+
) {
|
|
72
|
+
errors.push(
|
|
73
|
+
`${tier}.deploymentStrategy must be one of: ${VALID_STRATEGIES.join(', ')}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (tierRules.requiresApproval !== undefined && typeof tierRules.requiresApproval !== 'boolean') {
|
|
78
|
+
errors.push(`${tier}.requiresApproval must be a boolean`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (tierRules.requires2FA !== undefined && typeof tierRules.requires2FA !== 'boolean') {
|
|
82
|
+
errors.push(`${tier}.requires2FA must be a boolean`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
valid: errors.length === 0,
|
|
88
|
+
errors,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get rules for a specific tier
|
|
94
|
+
* @param {string} tier - Tier name (feature, dev, stable)
|
|
95
|
+
* @param {object} customRules - Optional custom rules to use instead of defaults
|
|
96
|
+
* @returns {object} Rules for the specified tier
|
|
97
|
+
*/
|
|
98
|
+
export function getRulesForTier(tier, customRules = null) {
|
|
99
|
+
const rules = customRules ? mergeWithDefaults(customRules) : DEFAULT_RULES;
|
|
100
|
+
return rules[tier] || DEFAULT_RULES.feature;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Merge custom rules with defaults
|
|
105
|
+
* @param {object} custom - Custom rules to merge
|
|
106
|
+
* @returns {object} Merged rules
|
|
107
|
+
*/
|
|
108
|
+
export function mergeWithDefaults(custom) {
|
|
109
|
+
if (!custom || Object.keys(custom).length === 0) {
|
|
110
|
+
return DEFAULT_RULES;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const merged = {};
|
|
114
|
+
|
|
115
|
+
for (const tier of Object.keys(DEFAULT_RULES)) {
|
|
116
|
+
if (custom[tier]) {
|
|
117
|
+
merged[tier] = {
|
|
118
|
+
...DEFAULT_RULES[tier],
|
|
119
|
+
...custom[tier],
|
|
120
|
+
};
|
|
121
|
+
} else {
|
|
122
|
+
merged[tier] = { ...DEFAULT_RULES[tier] };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return merged;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Factory function to create a deployment rules manager
|
|
131
|
+
* @param {object} config - Optional configuration object
|
|
132
|
+
* @returns {{ getRules: Function, validate: Function, getForTier: Function }}
|
|
133
|
+
*/
|
|
134
|
+
export function createDeploymentRules(config = null) {
|
|
135
|
+
const rules = loadDeploymentRules(config);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
/**
|
|
139
|
+
* Get all rules
|
|
140
|
+
* @returns {object} All deployment rules
|
|
141
|
+
*/
|
|
142
|
+
getRules() {
|
|
143
|
+
return rules;
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate the current rules
|
|
148
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
149
|
+
*/
|
|
150
|
+
validate() {
|
|
151
|
+
return validateRules(rules);
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get rules for a specific tier
|
|
156
|
+
* @param {string} tier - Tier name
|
|
157
|
+
* @returns {object} Rules for the tier
|
|
158
|
+
*/
|
|
159
|
+
getForTier(tier) {
|
|
160
|
+
return rules[tier] || DEFAULT_RULES.feature;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Rules Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
loadDeploymentRules,
|
|
7
|
+
validateRules,
|
|
8
|
+
getRulesForTier,
|
|
9
|
+
mergeWithDefaults,
|
|
10
|
+
DEFAULT_RULES,
|
|
11
|
+
createDeploymentRules,
|
|
12
|
+
} from './deployment-rules.js';
|
|
13
|
+
|
|
14
|
+
describe('deployment-rules', () => {
|
|
15
|
+
describe('DEFAULT_RULES', () => {
|
|
16
|
+
it('defines rules for all tiers', () => {
|
|
17
|
+
expect(DEFAULT_RULES.feature).toBeDefined();
|
|
18
|
+
expect(DEFAULT_RULES.dev).toBeDefined();
|
|
19
|
+
expect(DEFAULT_RULES.stable).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('feature tier auto-deploys with basic checks', () => {
|
|
23
|
+
expect(DEFAULT_RULES.feature.autoDeploy).toBe(true);
|
|
24
|
+
expect(DEFAULT_RULES.feature.securityGates).toContain('sast');
|
|
25
|
+
expect(DEFAULT_RULES.feature.securityGates).toContain('dependencies');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('dev tier auto-deploys with full checks', () => {
|
|
29
|
+
expect(DEFAULT_RULES.dev.autoDeploy).toBe(true);
|
|
30
|
+
expect(DEFAULT_RULES.dev.securityGates).toContain('sast');
|
|
31
|
+
expect(DEFAULT_RULES.dev.securityGates).toContain('dast');
|
|
32
|
+
expect(DEFAULT_RULES.dev.securityGates).toContain('container');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('stable tier requires approval', () => {
|
|
36
|
+
expect(DEFAULT_RULES.stable.autoDeploy).toBe(false);
|
|
37
|
+
expect(DEFAULT_RULES.stable.requiresApproval).toBe(true);
|
|
38
|
+
expect(DEFAULT_RULES.stable.requires2FA).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('loadDeploymentRules', () => {
|
|
43
|
+
it('loads rules from config object', () => {
|
|
44
|
+
const config = {
|
|
45
|
+
deployment: {
|
|
46
|
+
rules: {
|
|
47
|
+
feature: { autoDeploy: false },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
const rules = loadDeploymentRules(config);
|
|
52
|
+
expect(rules.feature.autoDeploy).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns defaults when no config', () => {
|
|
56
|
+
const rules = loadDeploymentRules({});
|
|
57
|
+
expect(rules).toEqual(DEFAULT_RULES);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns defaults for null config', () => {
|
|
61
|
+
const rules = loadDeploymentRules(null);
|
|
62
|
+
expect(rules).toEqual(DEFAULT_RULES);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('validateRules', () => {
|
|
67
|
+
it('validates correct rule structure', () => {
|
|
68
|
+
const rules = {
|
|
69
|
+
feature: {
|
|
70
|
+
autoDeploy: true,
|
|
71
|
+
securityGates: ['sast'],
|
|
72
|
+
deploymentStrategy: 'rolling',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const result = validateRules(rules);
|
|
76
|
+
expect(result.valid).toBe(true);
|
|
77
|
+
expect(result.errors).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('rejects invalid autoDeploy type', () => {
|
|
81
|
+
const rules = {
|
|
82
|
+
feature: { autoDeploy: 'yes' },
|
|
83
|
+
};
|
|
84
|
+
const result = validateRules(rules);
|
|
85
|
+
expect(result.valid).toBe(false);
|
|
86
|
+
expect(result.errors.some(e => e.includes('autoDeploy'))).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rejects invalid security gates', () => {
|
|
90
|
+
const rules = {
|
|
91
|
+
feature: { securityGates: 'sast' },
|
|
92
|
+
};
|
|
93
|
+
const result = validateRules(rules);
|
|
94
|
+
expect(result.valid).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects unknown deployment strategy', () => {
|
|
98
|
+
const rules = {
|
|
99
|
+
feature: { deploymentStrategy: 'teleport' },
|
|
100
|
+
};
|
|
101
|
+
const result = validateRules(rules);
|
|
102
|
+
expect(result.valid).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('allows valid deployment strategies', () => {
|
|
106
|
+
const strategies = ['rolling', 'blue-green', 'canary', 'recreate'];
|
|
107
|
+
for (const strategy of strategies) {
|
|
108
|
+
const rules = { feature: { deploymentStrategy: strategy } };
|
|
109
|
+
const result = validateRules(rules);
|
|
110
|
+
expect(result.valid).toBe(true);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('getRulesForTier', () => {
|
|
116
|
+
it('returns rules for specified tier', () => {
|
|
117
|
+
const rules = getRulesForTier('stable');
|
|
118
|
+
expect(rules.requiresApproval).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('returns feature rules for unknown tier', () => {
|
|
122
|
+
const rules = getRulesForTier('unknown');
|
|
123
|
+
expect(rules).toEqual(DEFAULT_RULES.feature);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('accepts custom rules', () => {
|
|
127
|
+
const customRules = {
|
|
128
|
+
dev: { autoDeploy: false },
|
|
129
|
+
};
|
|
130
|
+
const rules = getRulesForTier('dev', customRules);
|
|
131
|
+
expect(rules.autoDeploy).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('mergeWithDefaults', () => {
|
|
136
|
+
it('merges custom rules with defaults', () => {
|
|
137
|
+
const custom = {
|
|
138
|
+
feature: { autoDeploy: false },
|
|
139
|
+
};
|
|
140
|
+
const merged = mergeWithDefaults(custom);
|
|
141
|
+
expect(merged.feature.autoDeploy).toBe(false);
|
|
142
|
+
expect(merged.feature.securityGates).toEqual(DEFAULT_RULES.feature.securityGates);
|
|
143
|
+
expect(merged.dev).toEqual(DEFAULT_RULES.dev);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('preserves nested arrays', () => {
|
|
147
|
+
const custom = {
|
|
148
|
+
dev: { securityGates: ['sast'] },
|
|
149
|
+
};
|
|
150
|
+
const merged = mergeWithDefaults(custom);
|
|
151
|
+
expect(merged.dev.securityGates).toEqual(['sast']);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('handles empty custom rules', () => {
|
|
155
|
+
const merged = mergeWithDefaults({});
|
|
156
|
+
expect(merged).toEqual(DEFAULT_RULES);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('createDeploymentRules', () => {
|
|
161
|
+
it('creates rules manager', () => {
|
|
162
|
+
const manager = createDeploymentRules();
|
|
163
|
+
expect(manager.getRules).toBeDefined();
|
|
164
|
+
expect(manager.validate).toBeDefined();
|
|
165
|
+
expect(manager.getForTier).toBeDefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('loads rules on creation', () => {
|
|
169
|
+
const config = {
|
|
170
|
+
deployment: {
|
|
171
|
+
rules: {
|
|
172
|
+
feature: { autoDeploy: false },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const manager = createDeploymentRules(config);
|
|
177
|
+
expect(manager.getForTier('feature').autoDeploy).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('exposes all rules', () => {
|
|
181
|
+
const manager = createDeploymentRules();
|
|
182
|
+
const rules = manager.getRules();
|
|
183
|
+
expect(rules.feature).toBeDefined();
|
|
184
|
+
expect(rules.dev).toBeDefined();
|
|
185
|
+
expect(rules.stable).toBeDefined();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|