pi-cicd 0.3.0 → 1.0.1
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 +19 -0
- package/README.md +34 -40
- package/docs/API.md +61 -0
- package/docs/COMMANDS.md +138 -0
- package/docs/CONFIG.md +123 -0
- package/docs/GUIDE.md +171 -0
- package/docs/PATTERNS.md +49 -0
- package/docs/QUICKSTART.md +99 -0
- package/{dist/index.d.ts → index.ts} +26 -4
- package/install.mjs +34 -0
- package/package.json +21 -21
- package/skills/intelligent-deploy/SKILL.md +229 -0
- package/src/ci/pipeline.ts +130 -0
- package/src/ci/pr-creator.ts +74 -0
- package/src/ci/report.ts +65 -0
- package/src/ci/test-runner.ts +129 -0
- package/src/config.ts +99 -0
- package/src/deploy/canary-deploy.ts +211 -0
- package/src/deploy/landing-queue.ts +222 -0
- package/src/headless/answer-injector.ts +99 -0
- package/src/headless/exit-codes.ts +32 -0
- package/src/headless/idle-detector.ts +76 -0
- package/src/headless/jsonl-stream.ts +90 -0
- package/src/headless/orchestrator.ts +207 -0
- package/{dist/index.js → src/index.ts} +30 -9
- package/src/tools/ci_status.ts +137 -0
- package/src/types.ts +149 -0
- package/src/workflow/deployment-workflow.ts +153 -0
- package/dist/ci/pipeline.d.ts +0 -43
- package/dist/ci/pipeline.d.ts.map +0 -1
- package/dist/ci/pipeline.js +0 -107
- package/dist/ci/pipeline.js.map +0 -1
- package/dist/ci/pr-creator.d.ts +0 -17
- package/dist/ci/pr-creator.d.ts.map +0 -1
- package/dist/ci/pr-creator.js +0 -67
- package/dist/ci/pr-creator.js.map +0 -1
- package/dist/ci/report.d.ts +0 -14
- package/dist/ci/report.d.ts.map +0 -1
- package/dist/ci/report.js +0 -51
- package/dist/ci/report.js.map +0 -1
- package/dist/ci/test-runner.d.ts +0 -10
- package/dist/ci/test-runner.d.ts.map +0 -1
- package/dist/ci/test-runner.js +0 -111
- package/dist/ci/test-runner.js.map +0 -1
- package/dist/config.d.ts +0 -33
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -67
- package/dist/config.js.map +0 -1
- package/dist/deploy/canary-deploy.d.ts +0 -80
- package/dist/deploy/canary-deploy.d.ts.map +0 -1
- package/dist/deploy/canary-deploy.js +0 -145
- package/dist/deploy/canary-deploy.js.map +0 -1
- package/dist/deploy/landing-queue.d.ts +0 -83
- package/dist/deploy/landing-queue.d.ts.map +0 -1
- package/dist/deploy/landing-queue.js +0 -172
- package/dist/deploy/landing-queue.js.map +0 -1
- package/dist/headless/answer-injector.d.ts +0 -27
- package/dist/headless/answer-injector.d.ts.map +0 -1
- package/dist/headless/answer-injector.js +0 -80
- package/dist/headless/answer-injector.js.map +0 -1
- package/dist/headless/exit-codes.d.ts +0 -13
- package/dist/headless/exit-codes.d.ts.map +0 -1
- package/dist/headless/exit-codes.js +0 -29
- package/dist/headless/exit-codes.js.map +0 -1
- package/dist/headless/idle-detector.d.ts +0 -32
- package/dist/headless/idle-detector.d.ts.map +0 -1
- package/dist/headless/idle-detector.js +0 -62
- package/dist/headless/idle-detector.js.map +0 -1
- package/dist/headless/jsonl-stream.d.ts +0 -28
- package/dist/headless/jsonl-stream.d.ts.map +0 -1
- package/dist/headless/jsonl-stream.js +0 -65
- package/dist/headless/jsonl-stream.js.map +0 -1
- package/dist/headless/orchestrator.d.ts +0 -63
- package/dist/headless/orchestrator.d.ts.map +0 -1
- package/dist/headless/orchestrator.js +0 -156
- package/dist/headless/orchestrator.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/tools/ci_status.d.ts +0 -40
- package/dist/tools/ci_status.d.ts.map +0 -1
- package/dist/tools/ci_status.js +0 -110
- package/dist/tools/ci_status.js.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -17
- package/dist/types.js.map +0 -1
- package/dist/workflow/deployment-workflow.d.ts +0 -56
- package/dist/workflow/deployment-workflow.d.ts.map +0 -1
- package/dist/workflow/deployment-workflow.js +0 -95
- package/dist/workflow/deployment-workflow.js.map +0 -1
package/src/ci/report.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-ci — CI report generation.
|
|
3
|
+
*
|
|
4
|
+
* Produces JSONL or human-readable summary reports from collected CI events.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CIEvent, CIEndEvent, CICostEvent, CITestEvent } from "../types.ts";
|
|
8
|
+
import { isCIEndEvent, isCICostEvent, isCITestEvent } from "../headless/jsonl-stream.ts";
|
|
9
|
+
|
|
10
|
+
export interface ReportOptions {
|
|
11
|
+
format: "jsonl" | "summary";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a report from a list of CI events.
|
|
16
|
+
*/
|
|
17
|
+
export function generateReport(events: CIEvent[], options?: ReportOptions): string {
|
|
18
|
+
const format = options?.format ?? "jsonl";
|
|
19
|
+
|
|
20
|
+
if (format === "summary") {
|
|
21
|
+
return generateSummary(events);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// JSONL: one event per line
|
|
25
|
+
return events.map((e) => JSON.stringify(e)).join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate a human-readable summary.
|
|
30
|
+
*/
|
|
31
|
+
function generateSummary(events: CIEvent[]): string {
|
|
32
|
+
const lines: string[] = ["=== CI Run Summary ===", ""];
|
|
33
|
+
|
|
34
|
+
let totalTestsPassed = 0;
|
|
35
|
+
let totalTestsFailed = 0;
|
|
36
|
+
let totalCostUsd = 0;
|
|
37
|
+
let exitCode = -1;
|
|
38
|
+
let durationMs = 0;
|
|
39
|
+
|
|
40
|
+
for (const event of events) {
|
|
41
|
+
if (isCITestEvent(event)) {
|
|
42
|
+
totalTestsPassed += event.passed;
|
|
43
|
+
totalTestsFailed += event.failed;
|
|
44
|
+
}
|
|
45
|
+
if (isCICostEvent(event)) {
|
|
46
|
+
totalCostUsd += event.cost_usd;
|
|
47
|
+
}
|
|
48
|
+
if (isCIEndEvent(event)) {
|
|
49
|
+
exitCode = event.exit_code;
|
|
50
|
+
durationMs = event.duration_ms;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
lines.push(`Exit Code: ${exitCode}`);
|
|
55
|
+
lines.push(`Duration: ${(durationMs / 1000).toFixed(1)}s`);
|
|
56
|
+
lines.push(`Tests: ${totalTestsPassed} passed, ${totalTestsFailed} failed`);
|
|
57
|
+
if (totalCostUsd > 0) {
|
|
58
|
+
lines.push(`Cost: $${totalCostUsd.toFixed(4)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const status = exitCode === 0 ? "SUCCESS" : exitCode === 10 ? "BLOCKED" : exitCode === 11 ? "CANCELLED" : "ERROR";
|
|
62
|
+
lines.push(`Status: ${status}`);
|
|
63
|
+
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-ci — Test result parsing for TAP, Jest, and Vitest output formats.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TestSummary } from "../types.ts";
|
|
6
|
+
|
|
7
|
+
export type TestOutputFormat = "tap" | "jest" | "vitest";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse test output and return a summary.
|
|
11
|
+
*/
|
|
12
|
+
export function parseTestResults(output: string, format: TestOutputFormat): TestSummary {
|
|
13
|
+
if (!output || !output.trim()) {
|
|
14
|
+
return { passed: 0, failed: 0, total: 0, duration_ms: 0 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
switch (format) {
|
|
18
|
+
case "tap":
|
|
19
|
+
return parseTap(output);
|
|
20
|
+
case "jest":
|
|
21
|
+
return parseJest(output);
|
|
22
|
+
case "vitest":
|
|
23
|
+
return parseVitest(output);
|
|
24
|
+
default:
|
|
25
|
+
return { passed: 0, failed: 0, total: 0, duration_ms: 0 };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse TAP (Test Anything Protocol) output.
|
|
31
|
+
*
|
|
32
|
+
* Example:
|
|
33
|
+
* 1..5
|
|
34
|
+
* ok 1 - first test
|
|
35
|
+
* not ok 2 - failing test
|
|
36
|
+
* ok 3 - third test
|
|
37
|
+
*/
|
|
38
|
+
function parseTap(output: string): TestSummary {
|
|
39
|
+
let passed = 0;
|
|
40
|
+
let failed = 0;
|
|
41
|
+
|
|
42
|
+
const lines = output.split("\n");
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
if (trimmed.startsWith("ok ")) {
|
|
46
|
+
passed++;
|
|
47
|
+
} else if (trimmed.startsWith("not ok ")) {
|
|
48
|
+
failed++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
passed,
|
|
54
|
+
failed,
|
|
55
|
+
total: passed + failed,
|
|
56
|
+
duration_ms: 0, // TAP doesn't have standard duration
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse Jest-style test output.
|
|
62
|
+
*
|
|
63
|
+
* Looks for patterns like:
|
|
64
|
+
* Tests: 5 passed, 2 failed, 7 total
|
|
65
|
+
* Time: 3.456 s
|
|
66
|
+
*/
|
|
67
|
+
function parseJest(output: string): TestSummary {
|
|
68
|
+
let passed = 0;
|
|
69
|
+
let failed = 0;
|
|
70
|
+
let total = 0;
|
|
71
|
+
let durationMs = 0;
|
|
72
|
+
|
|
73
|
+
// Match test summary line
|
|
74
|
+
const testMatch = output.match(
|
|
75
|
+
/Tests:\s*(\d+)\s*passed(?:,\s*(\d+)\s*failed)?(?:,\s*(\d+)\s*total)?/,
|
|
76
|
+
);
|
|
77
|
+
if (testMatch) {
|
|
78
|
+
passed = parseInt(testMatch[1], 10);
|
|
79
|
+
failed = parseInt(testMatch[2] || "0", 10);
|
|
80
|
+
total = parseInt(testMatch[3] || String(passed + failed), 10);
|
|
81
|
+
} else {
|
|
82
|
+
// Fallback: count individual test lines
|
|
83
|
+
const passMatches = output.match(/✓|PASS/g);
|
|
84
|
+
const failMatches = output.match(/✕|FAIL/g);
|
|
85
|
+
passed = passMatches?.length ?? 0;
|
|
86
|
+
failed = failMatches?.length ?? 0;
|
|
87
|
+
total = passed + failed;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Match time
|
|
91
|
+
const timeMatch = output.match(/Time:\s*([\d.]+)\s*s/);
|
|
92
|
+
if (timeMatch) {
|
|
93
|
+
durationMs = Math.round(parseFloat(timeMatch[1]) * 1000);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { passed, failed, total, duration_ms: durationMs };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse Vitest-style test output.
|
|
101
|
+
*
|
|
102
|
+
* Looks for patterns like:
|
|
103
|
+
* Tests 5 passed | 2 failed (7)
|
|
104
|
+
* Duration 3.45s
|
|
105
|
+
*/
|
|
106
|
+
function parseVitest(output: string): TestSummary {
|
|
107
|
+
let passed = 0;
|
|
108
|
+
let failed = 0;
|
|
109
|
+
let total = 0;
|
|
110
|
+
let durationMs = 0;
|
|
111
|
+
|
|
112
|
+
// Match test summary line: "Tests 5 passed | 2 failed (7)"
|
|
113
|
+
const testMatch = output.match(
|
|
114
|
+
/Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?(?:\s*\((\d+)\))?/,
|
|
115
|
+
);
|
|
116
|
+
if (testMatch) {
|
|
117
|
+
passed = parseInt(testMatch[1], 10);
|
|
118
|
+
failed = parseInt(testMatch[2] || "0", 10);
|
|
119
|
+
total = parseInt(testMatch[3] || String(passed + failed), 10);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Match duration: "Duration 3.45s"
|
|
123
|
+
const durMatch = output.match(/Duration\s+([\d.]+)s/);
|
|
124
|
+
if (durMatch) {
|
|
125
|
+
durationMs = Math.round(parseFloat(durMatch[1]) * 1000);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { passed, failed, total, duration_ms: durationMs };
|
|
129
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-ci — Configuration loading and defaults.
|
|
3
|
+
*
|
|
4
|
+
* Reads `.pi/pi-ci.json` from the given directory (or cwd) and merges with defaults.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
export interface PiCiReportConfig {
|
|
11
|
+
format: "jsonl" | "summary";
|
|
12
|
+
includeCost: boolean;
|
|
13
|
+
includeEdits: boolean;
|
|
14
|
+
includeTests: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PiCiExitCodeConfig {
|
|
18
|
+
success: number;
|
|
19
|
+
error: number;
|
|
20
|
+
blocked: number;
|
|
21
|
+
cancelled: number;
|
|
22
|
+
needsInput: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PiCiConfig {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
idleTimeoutMs: number;
|
|
28
|
+
maxRetries: number;
|
|
29
|
+
retryBackoffMaxMs: number;
|
|
30
|
+
exitCodes: PiCiExitCodeConfig;
|
|
31
|
+
report: PiCiReportConfig;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_CONFIG: PiCiConfig = {
|
|
35
|
+
enabled: true,
|
|
36
|
+
idleTimeoutMs: 15_000,
|
|
37
|
+
maxRetries: 3,
|
|
38
|
+
retryBackoffMaxMs: 30_000,
|
|
39
|
+
exitCodes: {
|
|
40
|
+
success: 0,
|
|
41
|
+
error: 1,
|
|
42
|
+
blocked: 10,
|
|
43
|
+
cancelled: 11,
|
|
44
|
+
needsInput: 12,
|
|
45
|
+
},
|
|
46
|
+
report: {
|
|
47
|
+
format: "jsonl",
|
|
48
|
+
includeCost: true,
|
|
49
|
+
includeEdits: true,
|
|
50
|
+
includeTests: true,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Load pi-ci configuration from `.pi/pi-ci.json` (if present) merged with defaults.
|
|
56
|
+
*/
|
|
57
|
+
export async function loadCiConfig(cwd?: string): Promise<PiCiConfig> {
|
|
58
|
+
const dir = cwd ?? process.cwd();
|
|
59
|
+
const configPath = join(dir, ".pi", "pi-ci.json");
|
|
60
|
+
|
|
61
|
+
let text: string;
|
|
62
|
+
try {
|
|
63
|
+
text = await readFile(configPath, "utf-8");
|
|
64
|
+
} catch {
|
|
65
|
+
return { ...DEFAULT_CONFIG };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const raw: unknown = JSON.parse(text);
|
|
69
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
70
|
+
return { ...DEFAULT_CONFIG };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const user = raw as Record<string, unknown>;
|
|
74
|
+
return {
|
|
75
|
+
enabled: typeof user.enabled === "boolean" ? user.enabled : DEFAULT_CONFIG.enabled,
|
|
76
|
+
idleTimeoutMs:
|
|
77
|
+
typeof user.idleTimeoutMs === "number" ? user.idleTimeoutMs : DEFAULT_CONFIG.idleTimeoutMs,
|
|
78
|
+
maxRetries:
|
|
79
|
+
typeof user.maxRetries === "number" ? user.maxRetries : DEFAULT_CONFIG.maxRetries,
|
|
80
|
+
retryBackoffMaxMs:
|
|
81
|
+
typeof user.retryBackoffMaxMs === "number"
|
|
82
|
+
? user.retryBackoffMaxMs
|
|
83
|
+
: DEFAULT_CONFIG.retryBackoffMaxMs,
|
|
84
|
+
exitCodes: {
|
|
85
|
+
...DEFAULT_CONFIG.exitCodes,
|
|
86
|
+
...(typeof user.exitCodes === "object" && user.exitCodes !== null
|
|
87
|
+
? (user.exitCodes as Partial<PiCiExitCodeConfig>)
|
|
88
|
+
: {}),
|
|
89
|
+
},
|
|
90
|
+
report: {
|
|
91
|
+
...DEFAULT_CONFIG.report,
|
|
92
|
+
...(typeof user.report === "object" && user.report !== null
|
|
93
|
+
? (user.report as Partial<PiCiReportConfig>)
|
|
94
|
+
: {}),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { DEFAULT_CONFIG };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canary Deployment with Monitoring
|
|
3
|
+
* Gradual rollout with automatic rollback
|
|
4
|
+
* Based on gstack /canary pattern
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface CanaryConfig {
|
|
8
|
+
/** Initial traffic percentage for canary */
|
|
9
|
+
initialPercentage: number;
|
|
10
|
+
/** Increment per step */
|
|
11
|
+
incrementPercentage: number;
|
|
12
|
+
/** Time between increments (ms) */
|
|
13
|
+
stepInterval: number;
|
|
14
|
+
/** Total duration before full rollout (ms) */
|
|
15
|
+
totalDuration: number;
|
|
16
|
+
/** Metrics to monitor */
|
|
17
|
+
metrics: {
|
|
18
|
+
successRate: { min: number };
|
|
19
|
+
latency: { max: number };
|
|
20
|
+
errorRate: { max: number };
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CanaryMetrics {
|
|
25
|
+
timestamp: number;
|
|
26
|
+
successRate: number;
|
|
27
|
+
latency: number;
|
|
28
|
+
errorRate: number;
|
|
29
|
+
requests: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CanaryResult {
|
|
33
|
+
success: boolean;
|
|
34
|
+
finalPercentage: number;
|
|
35
|
+
metrics: CanaryMetrics[];
|
|
36
|
+
issues: string[];
|
|
37
|
+
rolledBack: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DeployTarget {
|
|
41
|
+
name: string;
|
|
42
|
+
url: string;
|
|
43
|
+
healthy: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Canary Deployment Manager
|
|
48
|
+
*/
|
|
49
|
+
export class CanaryDeploy {
|
|
50
|
+
private config: CanaryConfig;
|
|
51
|
+
private metricsHistory: CanaryMetrics[] = [];
|
|
52
|
+
|
|
53
|
+
constructor(config?: Partial<CanaryConfig>) {
|
|
54
|
+
this.config = {
|
|
55
|
+
initialPercentage: config?.initialPercentage ?? 10,
|
|
56
|
+
incrementPercentage: config?.incrementPercentage ?? 10,
|
|
57
|
+
stepInterval: config?.stepInterval ?? 60000, // 1 minute
|
|
58
|
+
totalDuration: config?.totalDuration ?? 300000, // 5 minutes
|
|
59
|
+
metrics: config?.metrics ?? {
|
|
60
|
+
successRate: { min: 95 },
|
|
61
|
+
latency: { max: 500 },
|
|
62
|
+
errorRate: { max: 5 },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Execute canary deployment
|
|
69
|
+
*/
|
|
70
|
+
async deploy(target: DeployTarget): Promise<CanaryResult> {
|
|
71
|
+
console.log(`Starting canary deployment to ${target.name}`);
|
|
72
|
+
console.log(`Initial: ${this.config.initialPercentage}% traffic`);
|
|
73
|
+
|
|
74
|
+
const issues: string[] = [];
|
|
75
|
+
let currentPercentage = this.config.initialPercentage;
|
|
76
|
+
let rolledBack = false;
|
|
77
|
+
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
|
|
80
|
+
while (Date.now() - startTime < this.config.totalDuration) {
|
|
81
|
+
// Get current metrics
|
|
82
|
+
const metrics = await this.getMetrics(target);
|
|
83
|
+
this.metricsHistory.push(metrics);
|
|
84
|
+
|
|
85
|
+
// Check for issues
|
|
86
|
+
const detectedIssues = this.checkMetrics(metrics);
|
|
87
|
+
if (detectedIssues.length > 0) {
|
|
88
|
+
issues.push(...detectedIssues);
|
|
89
|
+
console.log(`⚠️ Issues detected: ${detectedIssues.join(', ')}`);
|
|
90
|
+
|
|
91
|
+
// Auto-rollback on critical issues
|
|
92
|
+
if (metrics.successRate < 90 || metrics.errorRate > 10) {
|
|
93
|
+
console.log('🔴 Critical issues - rolling back!');
|
|
94
|
+
rolledBack = true;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Print status
|
|
100
|
+
console.log(`[${Math.round((Date.now() - startTime) / 1000)}s] ` +
|
|
101
|
+
`Traffic: ${currentPercentage}% | ` +
|
|
102
|
+
`Success: ${metrics.successRate.toFixed(1)}% | ` +
|
|
103
|
+
`Latency: ${metrics.latency.toFixed(0)}ms`);
|
|
104
|
+
|
|
105
|
+
// Wait for next step
|
|
106
|
+
await this.delay(this.config.stepInterval);
|
|
107
|
+
|
|
108
|
+
// Increment traffic
|
|
109
|
+
currentPercentage = Math.min(100, currentPercentage + this.config.incrementPercentage);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
success: !rolledBack,
|
|
114
|
+
finalPercentage: rolledBack ? 0 : currentPercentage,
|
|
115
|
+
metrics: this.metricsHistory,
|
|
116
|
+
issues,
|
|
117
|
+
rolledBack,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get current metrics from target
|
|
123
|
+
*/
|
|
124
|
+
private async getMetrics(target: DeployTarget): Promise<CanaryMetrics> {
|
|
125
|
+
// Simulate metrics collection
|
|
126
|
+
// In production: call monitoring API
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
timestamp: Date.now(),
|
|
130
|
+
successRate: 95 + Math.random() * 5,
|
|
131
|
+
latency: 50 + Math.random() * 100,
|
|
132
|
+
errorRate: Math.random() * 3,
|
|
133
|
+
requests: Math.floor(100 + Math.random() * 900),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check metrics against thresholds
|
|
139
|
+
*/
|
|
140
|
+
private checkMetrics(metrics: CanaryMetrics): string[] {
|
|
141
|
+
const issues: string[] = [];
|
|
142
|
+
|
|
143
|
+
if (metrics.successRate < this.config.metrics.successRate.min) {
|
|
144
|
+
issues.push(`Low success rate: ${metrics.successRate.toFixed(1)}%`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (metrics.latency > this.config.metrics.latency.max) {
|
|
148
|
+
issues.push(`High latency: ${metrics.latency.toFixed(0)}ms`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (metrics.errorRate > this.config.metrics.errorRate.max) {
|
|
152
|
+
issues.push(`High error rate: ${metrics.errorRate.toFixed(1)}%`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return issues;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Rollback deployment
|
|
160
|
+
*/
|
|
161
|
+
async rollback(target: DeployTarget): Promise<void> {
|
|
162
|
+
console.log(`Rolling back ${target.name}`);
|
|
163
|
+
// In production: call rollback API
|
|
164
|
+
await this.delay(1000);
|
|
165
|
+
console.log('Rollback complete');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get deployment history
|
|
170
|
+
*/
|
|
171
|
+
getHistory(): CanaryMetrics[] {
|
|
172
|
+
return [...this.metricsHistory];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Generate deployment report
|
|
177
|
+
*/
|
|
178
|
+
formatReport(result: CanaryResult): string {
|
|
179
|
+
const lines: string[] = [];
|
|
180
|
+
|
|
181
|
+
lines.push('## Canary Deployment Report\n');
|
|
182
|
+
lines.push(`**Status:** ${result.success ? '✅ SUCCESS' : '❌ FAILED'}`);
|
|
183
|
+
lines.push(`**Final Traffic:** ${result.finalPercentage}%`);
|
|
184
|
+
lines.push(`**Rolled Back:** ${result.rolledBack ? 'Yes' : 'No'}\n`);
|
|
185
|
+
|
|
186
|
+
if (result.metrics.length > 0) {
|
|
187
|
+
lines.push('### Metrics History\n');
|
|
188
|
+
lines.push('| Time | Success | Latency | Error Rate |');
|
|
189
|
+
lines.push('|------|---------|---------|------------|');
|
|
190
|
+
|
|
191
|
+
result.metrics.forEach((m, i) => {
|
|
192
|
+
lines.push(`| ${i * 1}min | ${m.successRate.toFixed(1)}% | ${m.latency.toFixed(0)}ms | ${m.errorRate.toFixed(1)}% |`);
|
|
193
|
+
});
|
|
194
|
+
lines.push('');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (result.issues.length > 0) {
|
|
198
|
+
lines.push('### Issues Detected\n');
|
|
199
|
+
for (const issue of result.issues) {
|
|
200
|
+
lines.push(`- ${issue}`);
|
|
201
|
+
}
|
|
202
|
+
lines.push('');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return lines.join('\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private delay(ms: number): Promise<void> {
|
|
209
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
210
|
+
}
|
|
211
|
+
}
|