gitship-core 0.0.1 → 0.0.3

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/dist/queue.js ADDED
@@ -0,0 +1,102 @@
1
+ import { nanoid } from "nanoid";
2
+ import { createDeployment, getProject, getQueuedDeployments, getRunningDeployment, updateDeploymentStatus, getDeployment, appendDeploymentLog, } from "./db.js";
3
+ import { runDeploymentPipeline } from "./engine.js";
4
+ // Global map to track active execa processes for cancelling running tasks
5
+ export const activeProcesses = new Map();
6
+ export async function enqueueDeployment(projectId, branch, commitSha, commitMessage, author, rollbackOfId = null) {
7
+ const deploymentId = `dep_${nanoid(10)}`;
8
+ const deployment = createDeployment({
9
+ id: deploymentId,
10
+ project_id: projectId,
11
+ branch,
12
+ commit_sha: commitSha,
13
+ commit_message: commitMessage,
14
+ author,
15
+ status: "QUEUED",
16
+ started_at: null,
17
+ finished_at: null,
18
+ total_duration_ms: null,
19
+ rollback_of_id: rollbackOfId,
20
+ });
21
+ // Trigger queue processing asynchronously
22
+ processQueue(projectId).catch(err => {
23
+ console.error(`Error processing queue for project ${projectId}:`, err);
24
+ });
25
+ return deployment;
26
+ }
27
+ export async function processQueue(projectId) {
28
+ // 1. Check if there is already a running deployment for this project
29
+ const running = getRunningDeployment(projectId);
30
+ if (running) {
31
+ // A deployment is already running, wait for it to finish.
32
+ // It will trigger processQueue again upon completion.
33
+ return;
34
+ }
35
+ // 2. Get the next queued deployment
36
+ const queued = getQueuedDeployments(projectId);
37
+ if (queued.length === 0) {
38
+ return;
39
+ }
40
+ const nextDeployment = queued[0];
41
+ const project = getProject(projectId);
42
+ if (!project) {
43
+ console.error(`Project ${projectId} not found for deployment ${nextDeployment.id}`);
44
+ updateDeploymentStatus(nextDeployment.id, "FAILED", {
45
+ finished_at: Date.now(),
46
+ total_duration_ms: 0,
47
+ });
48
+ return;
49
+ }
50
+ try {
51
+ // Execute the deployment pipeline
52
+ await runDeploymentPipeline(project, nextDeployment);
53
+ }
54
+ catch (err) {
55
+ console.error(`Pipeline exception for deployment ${nextDeployment.id}:`, err);
56
+ }
57
+ finally {
58
+ // Process the next item in the queue
59
+ processQueue(projectId).catch(err => {
60
+ console.error(`Error in queue recursion for project ${projectId}:`, err);
61
+ });
62
+ }
63
+ }
64
+ export function cancelDeployment(id) {
65
+ const dep = getDeployment(id);
66
+ if (!dep) {
67
+ return { success: false, message: "Deployment not found" };
68
+ }
69
+ if (dep.status === "SUCCESS" || dep.status === "FAILED" || dep.status === "CANCELLED") {
70
+ return { success: false, message: `Deployment already finished with status: ${dep.status}` };
71
+ }
72
+ if (dep.status === "QUEUED") {
73
+ updateDeploymentStatus(id, "CANCELLED", {
74
+ finished_at: Date.now(),
75
+ total_duration_ms: 0,
76
+ });
77
+ appendDeploymentLog(id, "\n=== Deployment CANCELLED while in queue ===\n");
78
+ return { success: true, message: "Queued deployment cancelled successfully" };
79
+ }
80
+ if (dep.status === "RUNNING") {
81
+ // Set status to CANCELLED in DB first
82
+ updateDeploymentStatus(id, "CANCELLED", {
83
+ finished_at: Date.now(),
84
+ });
85
+ appendDeploymentLog(id, "\n=== Deployment CANCELLATION REQUESTED ===\n");
86
+ // Check if we have an active process to kill
87
+ const proc = activeProcesses.get(id);
88
+ if (proc) {
89
+ try {
90
+ proc.kill();
91
+ activeProcesses.delete(id);
92
+ return { success: true, message: "Running deployment cancelled and terminated" };
93
+ }
94
+ catch (err) {
95
+ return { success: true, message: `Running deployment status set to CANCELLED, but failed to kill process: ${err.message}` };
96
+ }
97
+ }
98
+ return { success: true, message: "Running deployment marked as CANCELLED" };
99
+ }
100
+ return { success: false, message: "Unknown status" };
101
+ }
102
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "gitship-core",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
7
10
  "scripts": {
8
11
  "build": "tsc",
9
12
  "test": "echo \"No tests for core\""
package/src/db.ts DELETED
@@ -1,425 +0,0 @@
1
- import Database from "better-sqlite3";
2
- import fs from "fs";
3
- import { DB_PATH, ensureDirsExist } from "./paths.js";
4
- import {
5
- Project,
6
- Webhook,
7
- Deployment,
8
- DeploymentStep,
9
- DeploymentLog,
10
- DeploymentStatus,
11
- StepStatus,
12
- } from "gitship-shared";
13
-
14
- let dbInstance: Database.Database | null = null;
15
-
16
- export function getDb(): Database.Database {
17
- if (dbInstance) return dbInstance;
18
- ensureDirsExist();
19
- dbInstance = new Database(DB_PATH);
20
- try {
21
- fs.chmodSync(DB_PATH, 0o600);
22
- } catch {}
23
- dbInstance.pragma("journal_mode = WAL");
24
- dbInstance.pragma("foreign_keys = ON");
25
- initDb(dbInstance);
26
- return dbInstance;
27
- }
28
-
29
- function initDb(db: Database.Database) {
30
- db.exec(`
31
- CREATE TABLE IF NOT EXISTS projects (
32
- id TEXT PRIMARY KEY,
33
- name TEXT UNIQUE NOT NULL,
34
- owner TEXT NOT NULL,
35
- repo TEXT NOT NULL,
36
- branch TEXT NOT NULL,
37
- target_type TEXT NOT NULL,
38
- target_host TEXT,
39
- target_path TEXT NOT NULL,
40
- install_cmd TEXT,
41
- build_cmd TEXT,
42
- restart_cmd TEXT,
43
- healthcheck_path TEXT,
44
- healthcheck_port INTEGER,
45
- healthcheck_retries INTEGER,
46
- healthcheck_interval_ms INTEGER,
47
- healthcheck_timeout_ms INTEGER,
48
- webhook_secret TEXT NOT NULL,
49
- created_at INTEGER NOT NULL,
50
- updated_at INTEGER NOT NULL
51
- );
52
-
53
- CREATE TABLE IF NOT EXISTS webhooks (
54
- id TEXT PRIMARY KEY,
55
- project_id TEXT NOT NULL,
56
- github_webhook_id INTEGER,
57
- url TEXT NOT NULL,
58
- secret TEXT NOT NULL,
59
- active INTEGER NOT NULL DEFAULT 1,
60
- created_at INTEGER NOT NULL,
61
- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
62
- );
63
-
64
- CREATE TABLE IF NOT EXISTS deployments (
65
- id TEXT PRIMARY KEY,
66
- project_id TEXT NOT NULL,
67
- branch TEXT NOT NULL,
68
- commit_sha TEXT,
69
- commit_message TEXT,
70
- author TEXT,
71
- status TEXT NOT NULL,
72
- started_at INTEGER,
73
- finished_at INTEGER,
74
- total_duration_ms INTEGER,
75
- rollback_of_id TEXT,
76
- created_at INTEGER NOT NULL,
77
- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
78
- );
79
-
80
- CREATE TABLE IF NOT EXISTS deployment_steps (
81
- id TEXT PRIMARY KEY,
82
- deployment_id TEXT NOT NULL,
83
- step_name TEXT NOT NULL,
84
- status TEXT NOT NULL,
85
- started_at INTEGER,
86
- finished_at INTEGER,
87
- duration_ms INTEGER,
88
- FOREIGN KEY (deployment_id) REFERENCES deployments(id) ON DELETE CASCADE
89
- );
90
-
91
- CREATE TABLE IF NOT EXISTS deployment_logs (
92
- deployment_id TEXT PRIMARY KEY,
93
- log_data TEXT NOT NULL,
94
- FOREIGN KEY (deployment_id) REFERENCES deployments(id) ON DELETE CASCADE
95
- );
96
-
97
- CREATE TABLE IF NOT EXISTS webhook_deliveries (
98
- id TEXT PRIMARY KEY,
99
- created_at INTEGER NOT NULL
100
- );
101
- `);
102
- }
103
-
104
- // Project Repositories
105
- export function addProject(project: Omit<Project, "created_at" | "updated_at">): Project {
106
- const db = getDb();
107
- const now = Date.now();
108
- const fullProject = { ...project, created_at: now, updated_at: now };
109
- const stmt = db.prepare(`
110
- INSERT INTO projects (id, name, owner, repo, branch, target_type, target_host, target_path, install_cmd, build_cmd, restart_cmd, healthcheck_path, healthcheck_port, healthcheck_retries, healthcheck_interval_ms, healthcheck_timeout_ms, webhook_secret, created_at, updated_at)
111
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
112
- ON CONFLICT(name) DO UPDATE SET
113
- owner = excluded.owner,
114
- repo = excluded.repo,
115
- branch = excluded.branch,
116
- target_type = excluded.target_type,
117
- target_host = excluded.target_host,
118
- target_path = excluded.target_path,
119
- install_cmd = excluded.install_cmd,
120
- build_cmd = excluded.build_cmd,
121
- restart_cmd = excluded.restart_cmd,
122
- healthcheck_path = excluded.healthcheck_path,
123
- healthcheck_port = excluded.healthcheck_port,
124
- healthcheck_retries = excluded.healthcheck_retries,
125
- healthcheck_interval_ms = excluded.healthcheck_interval_ms,
126
- healthcheck_timeout_ms = excluded.healthcheck_timeout_ms,
127
- webhook_secret = excluded.webhook_secret,
128
- updated_at = excluded.updated_at
129
- `);
130
- stmt.run(
131
- fullProject.id,
132
- fullProject.name,
133
- fullProject.owner,
134
- fullProject.repo,
135
- fullProject.branch,
136
- fullProject.target_type,
137
- fullProject.target_host || null,
138
- fullProject.target_path,
139
- fullProject.install_cmd || null,
140
- fullProject.build_cmd || null,
141
- fullProject.restart_cmd || null,
142
- fullProject.healthcheck_path || null,
143
- fullProject.healthcheck_port || null,
144
- fullProject.healthcheck_retries || null,
145
- fullProject.healthcheck_interval_ms || null,
146
- fullProject.healthcheck_timeout_ms || null,
147
- fullProject.webhook_secret,
148
- fullProject.created_at,
149
- fullProject.updated_at
150
- );
151
- return fullProject;
152
- }
153
-
154
- export function getProject(idOrName: string): Project | null {
155
- const db = getDb();
156
- const stmt = db.prepare("SELECT * FROM projects WHERE id = ? OR name = ?");
157
- const res = stmt.get(idOrName, idOrName) as Project | undefined;
158
- return res || null;
159
- }
160
-
161
- export function getProjects(): Project[] {
162
- const db = getDb();
163
- const stmt = db.prepare("SELECT * FROM projects ORDER BY name ASC");
164
- return stmt.all() as Project[];
165
- }
166
-
167
- export function removeProject(id: string): void {
168
- const db = getDb();
169
- const stmt = db.prepare("DELETE FROM projects WHERE id = ?");
170
- stmt.run(id);
171
- }
172
-
173
- // Webhook Repositories
174
- export function saveWebhook(webhook: Webhook): void {
175
- const db = getDb();
176
- const stmt = db.prepare(`
177
- INSERT INTO webhooks (id, project_id, github_webhook_id, url, secret, active, created_at)
178
- VALUES (?, ?, ?, ?, ?, ?, ?)
179
- ON CONFLICT(id) DO UPDATE SET
180
- github_webhook_id = excluded.github_webhook_id,
181
- url = excluded.url,
182
- secret = excluded.secret,
183
- active = excluded.active
184
- `);
185
- stmt.run(
186
- webhook.id,
187
- webhook.project_id,
188
- webhook.github_webhook_id,
189
- webhook.url,
190
- webhook.secret,
191
- webhook.active ? 1 : 0,
192
- webhook.created_at
193
- );
194
- }
195
-
196
- export function getWebhookByProjectId(projectId: string): Webhook | null {
197
- const db = getDb();
198
- const stmt = db.prepare("SELECT * FROM webhooks WHERE project_id = ?");
199
- const res = stmt.get(projectId) as any;
200
- if (!res) return null;
201
- return {
202
- ...res,
203
- active: res.active === 1,
204
- };
205
- }
206
-
207
- // Deployment Repositories
208
- export function createDeployment(deployment: Omit<Deployment, "created_at">): Deployment {
209
- const db = getDb();
210
- const now = Date.now();
211
- const fullDeployment = { ...deployment, created_at: now };
212
- const stmt = db.prepare(`
213
- INSERT INTO deployments (id, project_id, branch, commit_sha, commit_message, author, status, started_at, finished_at, total_duration_ms, rollback_of_id, created_at)
214
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
215
- `);
216
- stmt.run(
217
- fullDeployment.id,
218
- fullDeployment.project_id,
219
- fullDeployment.branch,
220
- fullDeployment.commit_sha || null,
221
- fullDeployment.commit_message || null,
222
- fullDeployment.author || null,
223
- fullDeployment.status,
224
- fullDeployment.started_at || null,
225
- fullDeployment.finished_at || null,
226
- fullDeployment.total_duration_ms || null,
227
- fullDeployment.rollback_of_id || null,
228
- fullDeployment.created_at
229
- );
230
-
231
- // Initialize empty logs for this deployment
232
- const logStmt = db.prepare("INSERT INTO deployment_logs (deployment_id, log_data) VALUES (?, ?)");
233
- logStmt.run(fullDeployment.id, "");
234
-
235
- return fullDeployment;
236
- }
237
-
238
- export function updateDeploymentStatus(
239
- id: string,
240
- status: DeploymentStatus,
241
- fields: Partial<Deployment> = {}
242
- ): void {
243
- const db = getDb();
244
- const updates: string[] = ["status = ?"];
245
- const params: any[] = [status];
246
-
247
- for (const [key, val] of Object.entries(fields)) {
248
- if (key !== "id" && key !== "status") {
249
- updates.push(`${key} = ?`);
250
- params.push(val);
251
- }
252
- }
253
-
254
- params.push(id);
255
- const stmt = db.prepare(`UPDATE deployments SET ${updates.join(", ")} WHERE id = ?`);
256
- stmt.run(...params);
257
- }
258
-
259
- export function getDeployment(id: string): Deployment | null {
260
- const db = getDb();
261
- const stmt = db.prepare("SELECT * FROM deployments WHERE id = ?");
262
- const res = stmt.get(id) as Deployment | undefined;
263
- return res || null;
264
- }
265
-
266
- export function getDeployments(projectId?: string, limit?: number): Deployment[] {
267
- const db = getDb();
268
- let query = "SELECT * FROM deployments";
269
- const params: any[] = [];
270
-
271
- if (projectId) {
272
- query += " WHERE project_id = ?";
273
- params.push(projectId);
274
- }
275
-
276
- query += " ORDER BY created_at DESC";
277
-
278
- if (limit) {
279
- query += " LIMIT ?";
280
- params.push(limit);
281
- }
282
-
283
- const stmt = db.prepare(query);
284
- return stmt.all(...params) as Deployment[];
285
- }
286
-
287
- export function getQueuedDeployments(projectId: string): Deployment[] {
288
- const db = getDb();
289
- const stmt = db.prepare("SELECT * FROM deployments WHERE project_id = ? AND status = 'QUEUED' ORDER BY created_at ASC");
290
- return stmt.all(projectId) as Deployment[];
291
- }
292
-
293
- export function getRunningDeployment(projectId: string): Deployment | null {
294
- const db = getDb();
295
- const stmt = db.prepare("SELECT * FROM deployments WHERE project_id = ? AND status = 'RUNNING'");
296
- const res = stmt.get(projectId) as Deployment | undefined;
297
- return res || null;
298
- }
299
-
300
- // Steps Repositories
301
- export function createDeploymentStep(step: DeploymentStep): void {
302
- const db = getDb();
303
- const stmt = db.prepare(`
304
- INSERT INTO deployment_steps (id, deployment_id, step_name, status, started_at, finished_at, duration_ms)
305
- VALUES (?, ?, ?, ?, ?, ?, ?)
306
- `);
307
- stmt.run(step.id, step.deployment_id, step.step_name, step.status, step.started_at, step.finished_at, step.duration_ms);
308
- }
309
-
310
- export function updateDeploymentStep(id: string, fields: Partial<DeploymentStep>): void {
311
- const db = getDb();
312
- const updates: string[] = [];
313
- const params: any[] = [];
314
-
315
- for (const [key, val] of Object.entries(fields)) {
316
- if (key !== "id") {
317
- updates.push(`${key} = ?`);
318
- params.push(val);
319
- }
320
- }
321
-
322
- params.push(id);
323
- const stmt = db.prepare(`UPDATE deployment_steps SET ${updates.join(", ")} WHERE id = ?`);
324
- stmt.run(...params);
325
- }
326
-
327
- export function getDeploymentSteps(deploymentId: string): DeploymentStep[] {
328
- const db = getDb();
329
- const stmt = db.prepare("SELECT * FROM deployment_steps WHERE deployment_id = ? ORDER BY started_at ASC");
330
- return stmt.all(deploymentId) as DeploymentStep[];
331
- }
332
-
333
- // Log Repositories
334
- export function appendDeploymentLog(deploymentId: string, text: string): void {
335
- const db = getDb();
336
- const stmt = db.prepare(`
337
- UPDATE deployment_logs
338
- SET log_data = log_data || ?
339
- WHERE deployment_id = ?
340
- `);
341
- stmt.run(text, deploymentId);
342
- }
343
-
344
- export function getDeploymentLog(deploymentId: string): string | null {
345
- const db = getDb();
346
- const stmt = db.prepare("SELECT log_data FROM deployment_logs WHERE deployment_id = ?");
347
- const res = stmt.get(deploymentId) as { log_data: string } | undefined;
348
- return res ? res.log_data : null;
349
- }
350
-
351
- // Stats Repository
352
- export interface ProjectStats {
353
- totalDeployments: number;
354
- successRate: number;
355
- avgDeployTimeMs: number;
356
- avgBuildTimeMs: number;
357
- slowestDeployMs: number;
358
- fastestDeployMs: number;
359
- }
360
-
361
- export function getStats(projectId?: string): ProjectStats {
362
- const db = getDb();
363
- const filter = projectId ? "WHERE project_id = ?" : "WHERE 1=1";
364
- const params = projectId ? [projectId] : [];
365
-
366
- const totalRow = db.prepare(`SELECT count(*) as count FROM deployments ${filter}`).get(...params) as { count: number };
367
- const total = totalRow.count;
368
-
369
- if (total === 0) {
370
- return {
371
- totalDeployments: 0,
372
- successRate: 0,
373
- avgDeployTimeMs: 0,
374
- avgBuildTimeMs: 0,
375
- slowestDeployMs: 0,
376
- fastestDeployMs: 0,
377
- };
378
- }
379
-
380
- const successRow = db.prepare(`SELECT count(*) as count FROM deployments ${filter} AND status = 'SUCCESS'`).get(...params) as { count: number };
381
- const successRate = total > 0 ? (successRow.count / total) * 100 : 0;
382
-
383
- const durationRow = db.prepare(`
384
- SELECT
385
- avg(total_duration_ms) as avg_duration,
386
- max(total_duration_ms) as max_duration,
387
- min(total_duration_ms) as min_duration
388
- FROM deployments
389
- ${filter} AND status = 'SUCCESS'
390
- `).get(...params) as { avg_duration: number | null; max_duration: number | null; min_duration: number | null };
391
-
392
- const buildDurationRow = db.prepare(`
393
- SELECT avg(ds.duration_ms) as avg_build
394
- FROM deployment_steps ds
395
- JOIN deployments d ON ds.deployment_id = d.id
396
- ${projectId ? "WHERE d.project_id = ?" : "WHERE 1=1"} AND ds.step_name = 'build' AND ds.status = 'SUCCESS'
397
- `).get(projectId ? [projectId] : []) as { avg_build: number | null };
398
-
399
- return {
400
- totalDeployments: total,
401
- successRate: parseFloat(successRate.toFixed(1)),
402
- avgDeployTimeMs: Math.round(durationRow.avg_duration || 0),
403
- avgBuildTimeMs: Math.round(buildDurationRow.avg_build || 0),
404
- slowestDeployMs: durationRow.max_duration || 0,
405
- fastestDeployMs: durationRow.min_duration || 0,
406
- };
407
- }
408
-
409
- export function isWebhookDeliveryProcessed(id: string): boolean {
410
- const db = getDb();
411
- const stmt = db.prepare("SELECT count(*) as count FROM webhook_deliveries WHERE id = ?");
412
- const res = stmt.get(id) as { count: number };
413
- return res.count > 0;
414
- }
415
-
416
- export function recordWebhookDelivery(id: string): void {
417
- const db = getDb();
418
- const now = Date.now();
419
- const insertStmt = db.prepare("INSERT OR IGNORE INTO webhook_deliveries (id, created_at) VALUES (?, ?)");
420
- insertStmt.run(id, now);
421
-
422
- // Prune older than 24 hours (86,400,000 ms)
423
- const pruneStmt = db.prepare("DELETE FROM webhook_deliveries WHERE created_at < ?");
424
- pruneStmt.run(now - 86400000);
425
- }