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.
Files changed (90) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +34 -40
  3. package/docs/API.md +61 -0
  4. package/docs/COMMANDS.md +138 -0
  5. package/docs/CONFIG.md +123 -0
  6. package/docs/GUIDE.md +171 -0
  7. package/docs/PATTERNS.md +49 -0
  8. package/docs/QUICKSTART.md +99 -0
  9. package/{dist/index.d.ts → index.ts} +26 -4
  10. package/install.mjs +34 -0
  11. package/package.json +21 -21
  12. package/skills/intelligent-deploy/SKILL.md +229 -0
  13. package/src/ci/pipeline.ts +130 -0
  14. package/src/ci/pr-creator.ts +74 -0
  15. package/src/ci/report.ts +65 -0
  16. package/src/ci/test-runner.ts +129 -0
  17. package/src/config.ts +99 -0
  18. package/src/deploy/canary-deploy.ts +211 -0
  19. package/src/deploy/landing-queue.ts +222 -0
  20. package/src/headless/answer-injector.ts +99 -0
  21. package/src/headless/exit-codes.ts +32 -0
  22. package/src/headless/idle-detector.ts +76 -0
  23. package/src/headless/jsonl-stream.ts +90 -0
  24. package/src/headless/orchestrator.ts +207 -0
  25. package/{dist/index.js → src/index.ts} +30 -9
  26. package/src/tools/ci_status.ts +137 -0
  27. package/src/types.ts +149 -0
  28. package/src/workflow/deployment-workflow.ts +153 -0
  29. package/dist/ci/pipeline.d.ts +0 -43
  30. package/dist/ci/pipeline.d.ts.map +0 -1
  31. package/dist/ci/pipeline.js +0 -107
  32. package/dist/ci/pipeline.js.map +0 -1
  33. package/dist/ci/pr-creator.d.ts +0 -17
  34. package/dist/ci/pr-creator.d.ts.map +0 -1
  35. package/dist/ci/pr-creator.js +0 -67
  36. package/dist/ci/pr-creator.js.map +0 -1
  37. package/dist/ci/report.d.ts +0 -14
  38. package/dist/ci/report.d.ts.map +0 -1
  39. package/dist/ci/report.js +0 -51
  40. package/dist/ci/report.js.map +0 -1
  41. package/dist/ci/test-runner.d.ts +0 -10
  42. package/dist/ci/test-runner.d.ts.map +0 -1
  43. package/dist/ci/test-runner.js +0 -111
  44. package/dist/ci/test-runner.js.map +0 -1
  45. package/dist/config.d.ts +0 -33
  46. package/dist/config.d.ts.map +0 -1
  47. package/dist/config.js +0 -67
  48. package/dist/config.js.map +0 -1
  49. package/dist/deploy/canary-deploy.d.ts +0 -80
  50. package/dist/deploy/canary-deploy.d.ts.map +0 -1
  51. package/dist/deploy/canary-deploy.js +0 -145
  52. package/dist/deploy/canary-deploy.js.map +0 -1
  53. package/dist/deploy/landing-queue.d.ts +0 -83
  54. package/dist/deploy/landing-queue.d.ts.map +0 -1
  55. package/dist/deploy/landing-queue.js +0 -172
  56. package/dist/deploy/landing-queue.js.map +0 -1
  57. package/dist/headless/answer-injector.d.ts +0 -27
  58. package/dist/headless/answer-injector.d.ts.map +0 -1
  59. package/dist/headless/answer-injector.js +0 -80
  60. package/dist/headless/answer-injector.js.map +0 -1
  61. package/dist/headless/exit-codes.d.ts +0 -13
  62. package/dist/headless/exit-codes.d.ts.map +0 -1
  63. package/dist/headless/exit-codes.js +0 -29
  64. package/dist/headless/exit-codes.js.map +0 -1
  65. package/dist/headless/idle-detector.d.ts +0 -32
  66. package/dist/headless/idle-detector.d.ts.map +0 -1
  67. package/dist/headless/idle-detector.js +0 -62
  68. package/dist/headless/idle-detector.js.map +0 -1
  69. package/dist/headless/jsonl-stream.d.ts +0 -28
  70. package/dist/headless/jsonl-stream.d.ts.map +0 -1
  71. package/dist/headless/jsonl-stream.js +0 -65
  72. package/dist/headless/jsonl-stream.js.map +0 -1
  73. package/dist/headless/orchestrator.d.ts +0 -63
  74. package/dist/headless/orchestrator.d.ts.map +0 -1
  75. package/dist/headless/orchestrator.js +0 -156
  76. package/dist/headless/orchestrator.js.map +0 -1
  77. package/dist/index.d.ts.map +0 -1
  78. package/dist/index.js.map +0 -1
  79. package/dist/tools/ci_status.d.ts +0 -40
  80. package/dist/tools/ci_status.d.ts.map +0 -1
  81. package/dist/tools/ci_status.js +0 -110
  82. package/dist/tools/ci_status.js.map +0 -1
  83. package/dist/types.d.ts +0 -93
  84. package/dist/types.d.ts.map +0 -1
  85. package/dist/types.js +0 -17
  86. package/dist/types.js.map +0 -1
  87. package/dist/workflow/deployment-workflow.d.ts +0 -56
  88. package/dist/workflow/deployment-workflow.d.ts.map +0 -1
  89. package/dist/workflow/deployment-workflow.js +0 -95
  90. package/dist/workflow/deployment-workflow.js.map +0 -1
@@ -0,0 +1,207 @@
1
+ /**
2
+ * pi-ci — Headless orchestrator.
3
+ *
4
+ * Ties together exit codes, answer injection, idle detection, and JSONL
5
+ * streaming into a single execution loop.
6
+ */
7
+
8
+ import type { ExitCode, CIEvent, CIOptions } from "../types.ts";
9
+ import { EXIT_CODES } from "../types.ts";
10
+ import { resolveExitCode } from "./exit-codes.ts";
11
+ import { matchAnswer } from "./answer-injector.ts";
12
+ import type { AnswerEntry } from "../types.ts";
13
+ import { IdleDetector } from "./idle-detector.ts";
14
+ import { CIEventCollector, writeCIEvent } from "./jsonl-stream.ts";
15
+ import type { Writable } from "node:stream";
16
+
17
+ export interface OrchestratorResult {
18
+ exitCode: ExitCode;
19
+ events: CIEvent[];
20
+ durationMs: number;
21
+ }
22
+
23
+ export interface OrchestratorHooks {
24
+ /** Called for each step of execution. Return a status string to signal completion. */
25
+ executeStep: (
26
+ prompt: string,
27
+ injectAnswer: (question: string) => string | undefined,
28
+ ) => Promise<StepResult>;
29
+ /** Optional writable for JSONL streaming. */
30
+ outputStream?: Writable;
31
+ }
32
+
33
+ export interface StepResult {
34
+ status: string;
35
+ edits?: { file: string; lines_added: number; lines_removed: number }[];
36
+ tests?: { command: string; passed: number; failed: number }[];
37
+ cost?: { tokens: { input: number; output: number }; cost_usd: number };
38
+ }
39
+
40
+ const RESTART_CONFIG = {
41
+ baseDelayMs: 5_000,
42
+ maxDelayMs: 30_000,
43
+ backoffMultiplier: 2,
44
+ };
45
+
46
+ export class HeadlessOrchestrator {
47
+ private readonly collector = new CIEventCollector();
48
+ private readonly answers: AnswerEntry[];
49
+ private readonly idleTimeoutMs: number;
50
+ private readonly maxRetries: number;
51
+ private readonly hooks: OrchestratorHooks;
52
+
53
+ constructor(
54
+ answers: AnswerEntry[],
55
+ options: CIOptions,
56
+ hooks: OrchestratorHooks,
57
+ ) {
58
+ this.answers = answers;
59
+ this.idleTimeoutMs = options.idleTimeoutMs ?? 15_000;
60
+ this.maxRetries = options.maxRetries ?? 3;
61
+ this.hooks = hooks;
62
+ }
63
+
64
+ /**
65
+ * Run the orchestrator loop.
66
+ *
67
+ * 1. Emit ci_start
68
+ * 2. Execute steps via the hook, checking for answer injection
69
+ * 3. On idle timeout → retry with exponential backoff
70
+ * 4. Emit ci_end with the resolved exit code
71
+ */
72
+ async run(prompt: string, mode: "single" | "plan"): Promise<OrchestratorResult> {
73
+ const startTime = Date.now();
74
+
75
+ const startEvent: CIEvent = {
76
+ type: "ci_start",
77
+ timestamp: new Date().toISOString(),
78
+ task: prompt,
79
+ mode,
80
+ };
81
+ this.emit(startEvent);
82
+
83
+ let lastExitCode: ExitCode = EXIT_CODES.ERROR;
84
+ let retries = 0;
85
+
86
+ while (retries <= this.maxRetries) {
87
+ const result = await this.runAttempt(prompt);
88
+ lastExitCode = result;
89
+
90
+ if (lastExitCode === EXIT_CODES.SUCCESS) break;
91
+ if (lastExitCode === EXIT_CODES.BLOCKED || lastExitCode === EXIT_CODES.CANCELLED) break;
92
+
93
+ // Retry on error/timeout
94
+ retries++;
95
+ if (retries <= this.maxRetries) {
96
+ const delay = Math.min(
97
+ RESTART_CONFIG.baseDelayMs *
98
+ Math.pow(RESTART_CONFIG.backoffMultiplier, retries - 1),
99
+ RESTART_CONFIG.maxDelayMs,
100
+ );
101
+ await sleep(delay);
102
+ }
103
+ }
104
+
105
+ const durationMs = Date.now() - startTime;
106
+ const endEvent: CIEvent = {
107
+ type: "ci_end",
108
+ timestamp: new Date().toISOString(),
109
+ exit_code: lastExitCode,
110
+ duration_ms: durationMs,
111
+ };
112
+ this.emit(endEvent);
113
+
114
+ return {
115
+ exitCode: lastExitCode,
116
+ events: this.collector.all(),
117
+ durationMs,
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Single attempt: run with idle detection.
123
+ */
124
+ private async runAttempt(prompt: string): Promise<ExitCode> {
125
+ return new Promise<ExitCode>((resolve) => {
126
+ let settled = false;
127
+
128
+ const idle = new IdleDetector({
129
+ idleTimeoutMs: this.idleTimeoutMs,
130
+ onTimeout: () => {
131
+ if (!settled) {
132
+ settled = true;
133
+ resolve(EXIT_CODES.TIMEOUT);
134
+ }
135
+ },
136
+ });
137
+
138
+ const injectAnswer = (question: string): string | undefined => {
139
+ idle.reset();
140
+ return matchAnswer(this.answers, question);
141
+ };
142
+
143
+ idle.start();
144
+
145
+ this.hooks
146
+ .executeStep(prompt, injectAnswer)
147
+ .then((stepResult) => {
148
+ if (!settled) {
149
+ settled = true;
150
+ idle.stop();
151
+
152
+ // Emit detail events
153
+ if (stepResult.edits) {
154
+ for (const edit of stepResult.edits) {
155
+ this.emit({
156
+ type: "ci_edit",
157
+ timestamp: new Date().toISOString(),
158
+ file: edit.file,
159
+ lines_added: edit.lines_added,
160
+ lines_removed: edit.lines_removed,
161
+ });
162
+ }
163
+ }
164
+ if (stepResult.tests) {
165
+ for (const t of stepResult.tests) {
166
+ this.emit({
167
+ type: "ci_test",
168
+ timestamp: new Date().toISOString(),
169
+ command: t.command,
170
+ passed: t.passed,
171
+ failed: t.failed,
172
+ });
173
+ }
174
+ }
175
+ if (stepResult.cost) {
176
+ this.emit({
177
+ type: "ci_cost",
178
+ timestamp: new Date().toISOString(),
179
+ tokens: stepResult.cost.tokens,
180
+ cost_usd: stepResult.cost.cost_usd,
181
+ });
182
+ }
183
+
184
+ resolve(resolveExitCode(stepResult.status));
185
+ }
186
+ })
187
+ .catch(() => {
188
+ if (!settled) {
189
+ settled = true;
190
+ idle.stop();
191
+ resolve(EXIT_CODES.ERROR);
192
+ }
193
+ });
194
+ });
195
+ }
196
+
197
+ private emit(event: CIEvent): void {
198
+ this.collector.emit(event);
199
+ if (this.hooks.outputStream) {
200
+ writeCIEvent(this.hooks.outputStream, event);
201
+ }
202
+ }
203
+ }
204
+
205
+ function sleep(ms: number): Promise<void> {
206
+ return new Promise((r) => setTimeout(r, ms));
207
+ }
@@ -3,7 +3,19 @@
3
3
  *
4
4
  * Registers the /ci status command and CI lifecycle hooks.
5
5
  */
6
- import { ciStatusHandler, } from "./src/tools/ci_status.ts";
6
+
7
+ import type { CIEvent, CIOptions, ExitCode } from "./src/types.ts";
8
+ import { EXIT_CODES } from "./src/types.ts";
9
+ import {
10
+ ciStatusHandler,
11
+ createRunTracker,
12
+ clearRuns,
13
+ registerRun,
14
+ type CIRunRecord,
15
+ } from "./src/tools/ci_status.ts";
16
+ import { CIPipeline, type PipelineResult } from "./src/ci/pipeline.ts";
17
+ import { generateReport } from "./src/ci/report.ts";
18
+
7
19
  // Re-export for consumers
8
20
  export { EXIT_CODES } from "./src/types.ts";
9
21
  export { resolveExitCode } from "./src/headless/exit-codes.ts";
@@ -17,15 +29,24 @@ export { parseTestResults } from "./src/ci/test-runner.ts";
17
29
  export { generateReport } from "./src/ci/report.ts";
18
30
  export { ciStatusHandler, registerRun, clearRuns, createRunTracker } from "./src/tools/ci_status.ts";
19
31
  export { loadCiConfig, DEFAULT_CONFIG } from "./src/config.ts";
32
+ export type { PiCiConfig } from "./src/config.ts";
33
+
34
+ /**
35
+ * Extension API type (minimal — avoids hard dep on pi-coding-agent types).
36
+ */
37
+ interface ExtensionAPI {
38
+ registerCommand?: (name: string, handler: (args: unknown) => string | Promise<string>) => void;
39
+ on?: (event: string, handler: (...args: unknown[]) => void) => void;
40
+ }
41
+
20
42
  /**
21
43
  * Default export — Pi extension registration.
22
44
  */
23
- export default function piCiExtension(pi) {
24
- // Register /ci status command
25
- if (pi.registerCommand) {
26
- pi.registerCommand("ci", (args) => {
27
- return ciStatusHandler(args);
28
- });
29
- }
45
+ export default function piCiExtension(pi: ExtensionAPI): void {
46
+ // Register /ci status command
47
+ if (pi.registerCommand) {
48
+ pi.registerCommand("ci", (args: unknown) => {
49
+ return ciStatusHandler(args);
50
+ });
51
+ }
30
52
  }
31
- //# sourceMappingURL=index.js.map
@@ -0,0 +1,137 @@
1
+ /**
2
+ * pi-ci — /ci status command handler.
3
+ *
4
+ * Shows the status of the current or last CI run.
5
+ */
6
+
7
+ import { CIEventCollector } from "../headless/jsonl-stream.ts";
8
+ import { generateReport } from "../ci/report.ts";
9
+ import type { CIEvent, CIEndEvent, ExitCode } from "../types.ts";
10
+ import { isCIEndEvent } from "../headless/jsonl-stream.ts";
11
+
12
+ export interface CIRunRecord {
13
+ id: string;
14
+ startTime: string;
15
+ events: CIEvent[];
16
+ exitCode?: ExitCode;
17
+ durationMs?: number;
18
+ }
19
+
20
+ /**
21
+ * Simple in-memory registry of CI runs (for the status command).
22
+ */
23
+ const runRegistry = new Map<string, CIRunRecord>();
24
+
25
+ /**
26
+ * Register a CI run for status lookups.
27
+ */
28
+ export function registerRun(record: CIRunRecord): void {
29
+ runRegistry.set(record.id, record);
30
+ }
31
+
32
+ /**
33
+ * Clear all registered runs (useful for testing).
34
+ */
35
+ export function clearRuns(): void {
36
+ runRegistry.clear();
37
+ }
38
+
39
+ /**
40
+ * Get a specific run by ID (prefix match supported).
41
+ */
42
+ export function getRun(id: string): CIRunRecord | undefined {
43
+ // Exact match first
44
+ if (runRegistry.has(id)) {
45
+ return runRegistry.get(id);
46
+ }
47
+ // Prefix match
48
+ for (const [key, value] of runRegistry) {
49
+ if (key.startsWith(id)) {
50
+ return value;
51
+ }
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ /**
57
+ * Handle the /ci status command.
58
+ *
59
+ * Returns a human-readable status string.
60
+ */
61
+ export function ciStatusHandler(args: unknown): string {
62
+ const runId = typeof args === "string" ? args : undefined;
63
+
64
+ if (runId) {
65
+ const run = getRun(runId);
66
+ if (!run) {
67
+ return `No CI run found matching: ${runId}`;
68
+ }
69
+ return formatRunStatus(run);
70
+ }
71
+
72
+ // Show all runs
73
+ if (runRegistry.size === 0) {
74
+ return "No CI runs found.";
75
+ }
76
+
77
+ const lines: string[] = [];
78
+ for (const run of runRegistry.values()) {
79
+ lines.push(formatRunStatus(run));
80
+ lines.push("");
81
+ }
82
+ return lines.join("\n").trimEnd();
83
+ }
84
+
85
+ function formatRunStatus(run: CIRunRecord): string {
86
+ const lines: string[] = [];
87
+ lines.push(`Run: ${run.id}`);
88
+ lines.push(`Started: ${run.startTime}`);
89
+
90
+ const endEvent = run.events.find((e): e is CIEndEvent => isCIEndEvent(e));
91
+ if (endEvent) {
92
+ lines.push(`Exit Code: ${endEvent.exit_code}`);
93
+ lines.push(`Duration: ${(endEvent.duration_ms / 1000).toFixed(1)}s`);
94
+
95
+ const status =
96
+ endEvent.exit_code === 0
97
+ ? "SUCCESS"
98
+ : endEvent.exit_code === 10
99
+ ? "BLOCKED"
100
+ : endEvent.exit_code === 11
101
+ ? "CANCELLED"
102
+ : "ERROR";
103
+ lines.push(`Status: ${status}`);
104
+ } else {
105
+ lines.push("Status: RUNNING");
106
+ }
107
+
108
+ return lines.join("\n");
109
+ }
110
+
111
+ /**
112
+ * Create a CI run tracker that collects events and registers the run.
113
+ */
114
+ export function createRunTracker(runId: string): {
115
+ collector: CIEventCollector;
116
+ finalize: () => CIRunRecord;
117
+ } {
118
+ const collector = new CIEventCollector();
119
+ const startTime = new Date().toISOString();
120
+
121
+ return {
122
+ collector,
123
+ finalize() {
124
+ const events = collector.all();
125
+ const endEvent = events.find((e): e is CIEndEvent => isCIEndEvent(e));
126
+ const record: CIRunRecord = {
127
+ id: runId,
128
+ startTime,
129
+ events,
130
+ exitCode: endEvent?.exit_code,
131
+ durationMs: endEvent?.duration_ms,
132
+ };
133
+ registerRun(record);
134
+ return record;
135
+ },
136
+ };
137
+ }
package/src/types.ts ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * pi-ci — Shared types for headless CI mode.
3
+ *
4
+ * Exit code contract and CI event types per SPEC.md §2.1 and §5.
5
+ */
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Exit codes
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export const EXIT_CODES = {
12
+ SUCCESS: 0,
13
+ ERROR: 1,
14
+ TIMEOUT: 1,
15
+ BLOCKED: 10,
16
+ CANCELLED: 11,
17
+ NEEDS_INPUT: 12,
18
+ } as const;
19
+
20
+ export type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // CI events (JSONL stream)
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export type CIEventType =
27
+ | "ci_start"
28
+ | "ci_progress"
29
+ | "ci_edit"
30
+ | "ci_test"
31
+ | "ci_cost"
32
+ | "ci_end";
33
+
34
+ export interface CIEventBase {
35
+ type: CIEventType;
36
+ timestamp: string; // ISO 8601
37
+ }
38
+
39
+ export interface CIStartEvent extends CIEventBase {
40
+ type: "ci_start";
41
+ task: string;
42
+ mode: "single" | "plan";
43
+ }
44
+
45
+ export type CIRunPhase =
46
+ | "exploring"
47
+ | "planning"
48
+ | "implementing"
49
+ | "verifying"
50
+ | "reviewing";
51
+
52
+ export interface CIProgressEvent extends CIEventBase {
53
+ type: "ci_progress";
54
+ phase: CIRunPhase;
55
+ files?: string[];
56
+ }
57
+
58
+ export interface CIEditEvent extends CIEventBase {
59
+ type: "ci_edit";
60
+ file: string;
61
+ lines_added: number;
62
+ lines_removed: number;
63
+ }
64
+
65
+ export interface CITestEvent extends CIEventBase {
66
+ type: "ci_test";
67
+ command: string;
68
+ passed: number;
69
+ failed: number;
70
+ }
71
+
72
+ export interface CICostEvent extends CIEventBase {
73
+ type: "ci_cost";
74
+ tokens: { input: number; output: number };
75
+ cost_usd: number;
76
+ }
77
+
78
+ export interface CIEndEvent extends CIEventBase {
79
+ type: "ci_end";
80
+ exit_code: ExitCode;
81
+ duration_ms: number;
82
+ }
83
+
84
+ export type CIEvent =
85
+ | CIStartEvent
86
+ | CIProgressEvent
87
+ | CIEditEvent
88
+ | CITestEvent
89
+ | CICostEvent
90
+ | CIEndEvent;
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Answer injection
94
+ // ---------------------------------------------------------------------------
95
+
96
+ export interface AnswerEntry {
97
+ match: string;
98
+ answer: string;
99
+ }
100
+
101
+ export interface AnswerFile {
102
+ answers: AnswerEntry[];
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Test results
107
+ // ---------------------------------------------------------------------------
108
+
109
+ export interface TestSummary {
110
+ passed: number;
111
+ failed: number;
112
+ total: number;
113
+ duration_ms: number;
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // PR creation
118
+ // ---------------------------------------------------------------------------
119
+
120
+ export interface PROptions {
121
+ title: string;
122
+ body?: string;
123
+ base?: string;
124
+ head?: string;
125
+ draft?: boolean;
126
+ labels?: string[];
127
+ }
128
+
129
+ export interface PRResult {
130
+ url: string;
131
+ number: number;
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Pipeline
136
+ // ---------------------------------------------------------------------------
137
+
138
+ export type CIPipelineMode = "single" | "plan" | "review" | "supervised";
139
+
140
+ export interface CIOptions {
141
+ prompt: string;
142
+ mode: CIPipelineMode;
143
+ answersFile?: string;
144
+ planFile?: string;
145
+ prNumber?: number;
146
+ resume?: string;
147
+ idleTimeoutMs?: number;
148
+ maxRetries?: number;
149
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Deployment Workflow - Pattern from pi-crew workflow-config.ts
3
+ *
4
+ * Declarative deployment workflow definition.
5
+ */
6
+
7
+ export interface DeploymentStep {
8
+ id: string;
9
+ name: string;
10
+ action: "validate" | "build" | "test" | "deploy" | "verify" | "rollback";
11
+ environment: "staging" | "production" | "preview";
12
+ dependsOn?: string[];
13
+ parallelGroup?: string;
14
+ timeout?: number; // ms
15
+ retry?: {
16
+ maxAttempts: number;
17
+ backoffMs: number;
18
+ };
19
+ conditions?: {
20
+ onlyIf?: string;
21
+ skipIf?: string;
22
+ };
23
+ }
24
+
25
+ export interface DeploymentWorkflow {
26
+ name: string;
27
+ description: string;
28
+ version: string;
29
+ steps: DeploymentStep[];
30
+ maxConcurrency?: number;
31
+ rollbackOnFailure?: boolean;
32
+ }
33
+
34
+ export interface DeploymentState {
35
+ workflowId: string;
36
+ runId: string;
37
+ status: "pending" | "running" | "completed" | "failed" | "rolled_back";
38
+ currentStep?: string;
39
+ completedSteps: string[];
40
+ failedSteps: string[];
41
+ startedAt: string;
42
+ finishedAt?: string;
43
+ }
44
+
45
+ /**
46
+ * Create a standard deployment workflow
47
+ */
48
+ export function createDeploymentWorkflow(
49
+ options: {
50
+ name: string;
51
+ environments?: string[];
52
+ includeStaging?: boolean;
53
+ }
54
+ ): DeploymentWorkflow {
55
+ const envs = options.environments ?? ["staging", "production"];
56
+
57
+ const steps: DeploymentStep[] = [
58
+ { id: "validate", name: "Validate", action: "validate", environment: "staging" as const },
59
+ { id: "build", name: "Build", action: "build", environment: "staging" as const, dependsOn: ["validate"] },
60
+ { id: "test", name: "Test", action: "test", environment: "staging" as const, dependsOn: ["build"] },
61
+ ];
62
+
63
+ if (options.includeStaging !== false) {
64
+ steps.push({
65
+ id: "deploy-staging",
66
+ name: "Deploy to Staging",
67
+ action: "deploy",
68
+ environment: "staging",
69
+ dependsOn: ["test"],
70
+ retry: { maxAttempts: 3, backoffMs: 5000 },
71
+ });
72
+ steps.push({
73
+ id: "verify-staging",
74
+ name: "Verify Staging",
75
+ action: "verify",
76
+ environment: "staging",
77
+ dependsOn: ["deploy-staging"],
78
+ });
79
+ }
80
+
81
+ if (envs.includes("production")) {
82
+ steps.push({
83
+ id: "deploy-production",
84
+ name: "Deploy to Production",
85
+ action: "deploy",
86
+ environment: "production",
87
+ dependsOn: options.includeStaging !== false ? ["verify-staging"] : ["test"],
88
+ retry: { maxAttempts: 3, backoffMs: 10000 },
89
+ conditions: {
90
+ onlyIf: "BRANCH=main",
91
+ },
92
+ });
93
+ }
94
+
95
+ return {
96
+ name: options.name,
97
+ description: `Deployment workflow for ${options.name}`,
98
+ version: "1.0.0",
99
+ steps,
100
+ maxConcurrency: 1,
101
+ rollbackOnFailure: true,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Execute a deployment workflow
107
+ */
108
+ export async function* executeDeploymentWorkflow(
109
+ workflow: DeploymentWorkflow,
110
+ executor: (step: DeploymentStep) => Promise<{ success: boolean; error?: string }>
111
+ ): AsyncGenerator<DeploymentState> {
112
+ const state: DeploymentState = {
113
+ workflowId: workflow.name,
114
+ runId: `deploy-${Date.now()}`,
115
+ status: "pending",
116
+ completedSteps: [],
117
+ failedSteps: [],
118
+ startedAt: new Date().toISOString(),
119
+ };
120
+
121
+ yield state;
122
+
123
+ const stepMap = new Map(workflow.steps.map(s => [s.id, s]));
124
+ const completed = new Set<string>();
125
+
126
+ for (const step of workflow.steps) {
127
+ // Check dependencies
128
+ if (step.dependsOn?.some(dep => !completed.has(dep))) {
129
+ continue; // Dependencies not met
130
+ }
131
+
132
+ state.status = "running";
133
+ state.currentStep = step.id;
134
+ yield state;
135
+
136
+ const result = await executor(step);
137
+
138
+ if (result.success) {
139
+ completed.add(step.id);
140
+ state.completedSteps.push(step.id);
141
+ } else {
142
+ state.failedSteps.push(step.id);
143
+ state.status = "failed";
144
+ state.finishedAt = new Date().toISOString();
145
+ yield state;
146
+ return;
147
+ }
148
+ }
149
+
150
+ state.status = "completed";
151
+ state.finishedAt = new Date().toISOString();
152
+ yield state;
153
+ }