gitship-core 0.0.2 → 0.0.4

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/src/engine.ts DELETED
@@ -1,477 +0,0 @@
1
- import { execa } from "execa";
2
- import fs from "fs";
3
- import path from "path";
4
- import { nanoid } from "nanoid";
5
- import {
6
- Project,
7
- Deployment,
8
- DeploymentStep,
9
- StepStatus,
10
- DeploymentStatus,
11
- } from "gitship-shared";
12
- import {
13
- updateDeploymentStatus,
14
- createDeploymentStep,
15
- updateDeploymentStep,
16
- appendDeploymentLog,
17
- getDeployment,
18
- } from "./db.js";
19
- import { BUILDS_DIR, readAuthConfig } from "./paths.js";
20
- import { activeProcesses } from "./queue.js";
21
-
22
- interface StepResult {
23
- status: StepStatus;
24
- durationMs: number;
25
- }
26
-
27
- export async function runDeploymentPipeline(
28
- project: Project,
29
- deployment: Deployment
30
- ): Promise<void> {
31
- const auth = readAuthConfig();
32
- const token = auth.github_token;
33
-
34
- if (!token) {
35
- const errorMsg = "Error: GitHub authentication token is missing. Please run 'deploykit auth github'.\n";
36
- appendDeploymentLog(deployment.id, errorMsg);
37
- updateDeploymentStatus(deployment.id, "FAILED", {
38
- finished_at: Date.now(),
39
- total_duration_ms: 0,
40
- });
41
- return;
42
- }
43
-
44
- // Prevent shell command injection via branch or commit SHA inputs
45
- const safeBranchRegex = /^[a-zA-Z0-9/_.-]+$/;
46
- if (!safeBranchRegex.test(deployment.branch)) {
47
- const errorMsg = `Error: Unsafe branch name format: "${deployment.branch}". Deployment aborted.\n`;
48
- appendDeploymentLog(deployment.id, errorMsg);
49
- updateDeploymentStatus(deployment.id, "FAILED", {
50
- finished_at: Date.now(),
51
- total_duration_ms: 0,
52
- });
53
- return;
54
- }
55
-
56
- if (deployment.commit_sha && !/^[a-zA-Z0-9]+$/.test(deployment.commit_sha)) {
57
- const errorMsg = `Error: Unsafe commit SHA format: "${deployment.commit_sha}". Deployment aborted.\n`;
58
- appendDeploymentLog(deployment.id, errorMsg);
59
- updateDeploymentStatus(deployment.id, "FAILED", {
60
- finished_at: Date.now(),
61
- total_duration_ms: 0,
62
- });
63
- return;
64
- }
65
-
66
- const startTime = Date.now();
67
- updateDeploymentStatus(deployment.id, "RUNNING", { started_at: startTime });
68
-
69
- appendDeploymentLog(
70
- deployment.id,
71
- `=== Starting deployment ${deployment.id} for project "${project.name}" ===\n` +
72
- `Branch: ${deployment.branch}\n` +
73
- `Commit: ${deployment.commit_sha || "latest"}\n` +
74
- `Author: ${deployment.author || "system"}\n` +
75
- `Target: ${project.target_type} (${
76
- project.target_type === "ssh"
77
- ? `${project.target_host}:${project.target_path}`
78
- : project.target_path
79
- })\n\n`
80
- );
81
-
82
- let status: DeploymentStatus = "SUCCESS";
83
-
84
- try {
85
- // Step 1: Clone or Pull
86
- const cloneSuccess = await runStep(
87
- deployment.id,
88
- "clone",
89
- async (log) => {
90
- // Double check cancellation
91
- checkCancelled(deployment.id);
92
-
93
- const repoUrl = `https://${token}@github.com/${project.owner}/${project.repo}.git`;
94
- if (project.target_type === "local") {
95
- const projectPath = path.join(BUILDS_DIR, project.name);
96
- if (!fs.existsSync(projectPath)) {
97
- log(`Cloning repository into local path ${projectPath}...\n`);
98
- await execLocal(
99
- deployment.id,
100
- "git",
101
- ["clone", "--branch", deployment.branch, repoUrl, "."],
102
- BUILDS_DIR,
103
- project.name, // create dir
104
- log
105
- );
106
- } else {
107
- log(`Repository directory exists. Fetching latest changes...\n`);
108
- await execLocal(deployment.id, "git", ["fetch", "origin"], projectPath, undefined, log);
109
-
110
- const checkoutTarget = deployment.commit_sha || `origin/${deployment.branch}`;
111
- log(`Checking out target: ${checkoutTarget}...\n`);
112
- await execLocal(deployment.id, "git", ["checkout", "-B", deployment.branch], projectPath, undefined, log);
113
- await execLocal(deployment.id, "git", ["reset", "--hard", checkoutTarget], projectPath, undefined, log);
114
- }
115
- } else {
116
- // SSH Target
117
- const port = project.target_host?.split(":")[1] || "22";
118
- const host = project.target_host?.split(":")[0] || "";
119
-
120
- log(`Ensuring target path ${project.target_path} exists on remote server...\n`);
121
- await execSSH(deployment.id, host, port, `mkdir -p ${project.target_path}`, log);
122
-
123
- // Check if git repository exists on the remote target path
124
- log(`Checking if repository is already initialized on remote...\n`);
125
- let isInitialized = false;
126
- try {
127
- await execSSH(deployment.id, host, port, `cd ${project.target_path} && git rev-parse --is-inside-work-tree`, () => {});
128
- isInitialized = true;
129
- } catch {
130
- isInitialized = false;
131
- }
132
-
133
- checkCancelled(deployment.id);
134
-
135
- if (!isInitialized) {
136
- log(`Cloning repository on remote server into ${project.target_path}...\n`);
137
- await execSSH(deployment.id, host, port, `git clone --branch ${deployment.branch} ${repoUrl} ${project.target_path}`, log);
138
- } else {
139
- log(`Fetching updates on remote server...\n`);
140
- await execSSH(deployment.id, host, port, `cd ${project.target_path} && git fetch origin`, log);
141
-
142
- const checkoutTarget = deployment.commit_sha || `origin/${deployment.branch}`;
143
- log(`Checking out target on remote server: ${checkoutTarget}...\n`);
144
- await execSSH(deployment.id, host, port, `cd ${project.target_path} && git checkout -B ${deployment.branch} && git reset --hard ${checkoutTarget}`, log);
145
- }
146
- }
147
- }
148
- );
149
-
150
- if (!cloneSuccess) throw new Error("Step 'clone' failed.");
151
-
152
- // Step 2: Install dependencies
153
- if (project.install_cmd) {
154
- const installSuccess = await runStep(
155
- deployment.id,
156
- "install",
157
- async (log) => {
158
- checkCancelled(deployment.id);
159
- log(`Running install command: ${project.install_cmd}\n`);
160
- if (project.target_type === "local") {
161
- const projectPath = path.join(BUILDS_DIR, project.name);
162
- await execShell(deployment.id, project.install_cmd!, projectPath, log);
163
- } else {
164
- const port = project.target_host?.split(":")[1] || "22";
165
- const host = project.target_host?.split(":")[0] || "";
166
- await execSSH(deployment.id, host, port, `cd ${project.target_path} && ${project.install_cmd}`, log);
167
- }
168
- }
169
- );
170
- if (!installSuccess) throw new Error("Step 'install' failed.");
171
- } else {
172
- skipStep(deployment.id, "install");
173
- }
174
-
175
- // Step 3: Build project
176
- if (project.build_cmd) {
177
- const buildSuccess = await runStep(
178
- deployment.id,
179
- "build",
180
- async (log) => {
181
- checkCancelled(deployment.id);
182
- log(`Running build command: ${project.build_cmd}\n`);
183
- if (project.target_type === "local") {
184
- const projectPath = path.join(BUILDS_DIR, project.name);
185
- await execShell(deployment.id, project.build_cmd!, projectPath, log);
186
- } else {
187
- const port = project.target_host?.split(":")[1] || "22";
188
- const host = project.target_host?.split(":")[0] || "";
189
- await execSSH(deployment.id, host, port, `cd ${project.target_path} && ${project.build_cmd}`, log);
190
- }
191
- }
192
- );
193
- if (!buildSuccess) throw new Error("Step 'build' failed.");
194
- } else {
195
- skipStep(deployment.id, "build");
196
- }
197
-
198
- // Step 4: Restart application
199
- if (project.restart_cmd) {
200
- const restartSuccess = await runStep(
201
- deployment.id,
202
- "restart",
203
- async (log) => {
204
- checkCancelled(deployment.id);
205
- log(`Running restart command: ${project.restart_cmd}\n`);
206
- if (project.target_type === "local") {
207
- const projectPath = path.join(BUILDS_DIR, project.name);
208
- await execShell(deployment.id, project.restart_cmd!, projectPath, log);
209
- } else {
210
- const port = project.target_host?.split(":")[1] || "22";
211
- const host = project.target_host?.split(":")[0] || "";
212
- await execSSH(deployment.id, host, port, `cd ${project.target_path} && ${project.restart_cmd}`, log);
213
- }
214
- }
215
- );
216
- if (!restartSuccess) throw new Error("Step 'restart' failed.");
217
- } else {
218
- skipStep(deployment.id, "restart");
219
- }
220
-
221
- // Step 5: Health Check
222
- if (project.healthcheck_path) {
223
- const healthSuccess = await runStep(
224
- deployment.id,
225
- "healthcheck",
226
- async (log) => {
227
- checkCancelled(deployment.id);
228
- const pathStr = project.healthcheck_path || "/";
229
- const portNum = project.healthcheck_port;
230
- const retries = project.healthcheck_retries || 5;
231
- const interval = project.healthcheck_interval_ms || 1000;
232
- const timeout = project.healthcheck_timeout_ms || 2000;
233
-
234
- const url = portNum ? `http://localhost:${portNum}${pathStr}` : `http://localhost${pathStr}`;
235
- log(`Performing health check on: ${url} (Retries: ${retries}, Interval: ${interval}ms)\n`);
236
-
237
- let lastError: any = null;
238
- for (let i = 1; i <= retries; i++) {
239
- checkCancelled(deployment.id);
240
- log(`Attempt ${i} of ${retries}... `);
241
-
242
- try {
243
- if (project.target_type === "local") {
244
- const controller = new AbortController();
245
- const timeoutId = setTimeout(() => controller.abort(), timeout);
246
- const res = await fetch(url, { signal: controller.signal });
247
- clearTimeout(timeoutId);
248
- if (res.ok) {
249
- log(`OK (${res.status})\n`);
250
- return;
251
- } else {
252
- throw new Error(`HTTP status ${res.status}`);
253
- }
254
- } else {
255
- // SSH Target
256
- const sshPort = project.target_host?.split(":")[1] || "22";
257
- const host = project.target_host?.split(":")[0] || "";
258
- const maxTimeSecs = Math.max(1, Math.round(timeout / 1000));
259
- await execSSH(
260
- deployment.id,
261
- host,
262
- sshPort,
263
- `curl -s -f --max-time ${maxTimeSecs} ${url}`,
264
- () => {}
265
- );
266
- log(`OK (exit code 0)\n`);
267
- return;
268
- }
269
- } catch (err: any) {
270
- lastError = err;
271
- log(`FAILED: ${err.message || err}\n`);
272
- if (i < retries) {
273
- await new Promise((r) => setTimeout(r, interval));
274
- }
275
- }
276
- }
277
- throw new Error(`Health check failed after ${retries} retries. Last error: ${lastError?.message || lastError}`);
278
- }
279
- );
280
- if (!healthSuccess) throw new Error("Step 'healthcheck' failed.");
281
- } else {
282
- skipStep(deployment.id, "healthcheck");
283
- }
284
-
285
- // Check if cancelled at the very end
286
- const currentDep = getDeployment(deployment.id);
287
- if (currentDep?.status === "CANCELLED") {
288
- status = "CANCELLED";
289
- } else {
290
- appendDeploymentLog(deployment.id, `\n=== Deployment SUCCESS ===\n`);
291
- }
292
- } catch (err: any) {
293
- const currentDep = getDeployment(deployment.id);
294
- if (currentDep?.status === "CANCELLED") {
295
- status = "CANCELLED";
296
- appendDeploymentLog(deployment.id, `\n=== Deployment CANCELLED ===\n`);
297
- } else {
298
- status = "FAILED";
299
- appendDeploymentLog(deployment.id, `\n=== Deployment FAILED ===\nReason: ${err.message || err}\n`);
300
- }
301
- } finally {
302
- const endTime = Date.now();
303
- const duration = endTime - startTime;
304
- updateDeploymentStatus(deployment.id, status, {
305
- finished_at: endTime,
306
- total_duration_ms: duration,
307
- });
308
- }
309
- }
310
-
311
- function checkCancelled(deploymentId: string) {
312
- const currentDep = getDeployment(deploymentId);
313
- if (currentDep?.status === "CANCELLED") {
314
- throw new Error("Deployment cancelled by user.");
315
- }
316
- }
317
-
318
- // Execa locally helper
319
- async function execLocal(
320
- deploymentId: string,
321
- cmd: string,
322
- args: string[],
323
- cwd: string,
324
- createDir?: string,
325
- log?: (chunk: string) => void
326
- ): Promise<void> {
327
- const finalCwd = createDir ? cwd : cwd;
328
- if (createDir) {
329
- fs.mkdirSync(path.join(cwd, createDir), { recursive: true });
330
- }
331
-
332
- const proc = execa(cmd, args, {
333
- cwd: createDir ? path.join(cwd, createDir) : finalCwd,
334
- all: true,
335
- });
336
-
337
- activeProcesses.set(deploymentId, proc);
338
-
339
- if (log && proc.all) {
340
- proc.all.on("data", (chunk) => {
341
- log(chunk.toString());
342
- });
343
- }
344
-
345
- try {
346
- await proc;
347
- } finally {
348
- activeProcesses.delete(deploymentId);
349
- }
350
- }
351
-
352
- // Execa locally with shell helper
353
- async function execShell(
354
- deploymentId: string,
355
- cmd: string,
356
- cwd: string,
357
- log: (chunk: string) => void
358
- ): Promise<void> {
359
- const proc = execa({ shell: true, cwd, all: true })`${cmd}`;
360
-
361
- activeProcesses.set(deploymentId, proc);
362
-
363
- if (proc.all) {
364
- proc.all.on("data", (chunk) => {
365
- log(chunk.toString());
366
- });
367
- }
368
-
369
- try {
370
- await proc;
371
- } finally {
372
- activeProcesses.delete(deploymentId);
373
- }
374
- }
375
-
376
- // Execa SSH command helper
377
- async function execSSH(
378
- deploymentId: string,
379
- host: string,
380
- port: string,
381
- cmd: string,
382
- log: (chunk: string) => void
383
- ): Promise<void> {
384
- // Execute via SSH CLI
385
- const proc = execa("ssh", ["-o", "StrictHostKeyChecking=no", "-p", port, host, cmd], {
386
- all: true,
387
- });
388
-
389
- activeProcesses.set(deploymentId, proc);
390
-
391
- if (proc.all) {
392
- proc.all.on("data", (chunk) => {
393
- log(chunk.toString());
394
- });
395
- }
396
-
397
- try {
398
- await proc;
399
- } finally {
400
- activeProcesses.delete(deploymentId);
401
- }
402
- }
403
-
404
- async function runStep(
405
- deploymentId: string,
406
- name: "clone" | "install" | "build" | "restart" | "healthcheck",
407
- fn: (log: (text: string) => void) => Promise<void>
408
- ): Promise<boolean> {
409
- const stepId = nanoid();
410
- const startTime = Date.now();
411
-
412
- const step: DeploymentStep = {
413
- id: stepId,
414
- deployment_id: deploymentId,
415
- step_name: name,
416
- status: "RUNNING",
417
- started_at: startTime,
418
- finished_at: null,
419
- duration_ms: null,
420
- };
421
-
422
- createDeploymentStep(step);
423
- appendDeploymentLog(deploymentId, `[Step: ${name}] Started...\n`);
424
-
425
- try {
426
- await fn((text: string) => {
427
- appendDeploymentLog(deploymentId, text);
428
- });
429
-
430
- // Check cancellation again
431
- checkCancelled(deploymentId);
432
-
433
- const endTime = Date.now();
434
- const duration = endTime - startTime;
435
-
436
- updateDeploymentStep(stepId, {
437
- status: "SUCCESS",
438
- finished_at: endTime,
439
- duration_ms: duration,
440
- });
441
- appendDeploymentLog(deploymentId, `[Step: ${name}] Completed successfully (${Math.round(duration / 1000)}s).\n\n`);
442
- return true;
443
- } catch (err: any) {
444
- const endTime = Date.now();
445
- const duration = endTime - startTime;
446
-
447
- const currentDep = getDeployment(deploymentId);
448
- const isCancelled = currentDep?.status === "CANCELLED";
449
-
450
- updateDeploymentStep(stepId, {
451
- status: isCancelled ? "FAILED" : "FAILED", // or can mark as cancelled if step structure supported it, but FAILED is robust
452
- finished_at: endTime,
453
- duration_ms: duration,
454
- });
455
-
456
- appendDeploymentLog(deploymentId, `[Step: ${name}] ${isCancelled ? 'Cancelled' : 'Failed'} (${Math.round(duration / 1000)}s).\nError detail: ${err.message || err}\n\n`);
457
- return false;
458
- }
459
- }
460
-
461
- function skipStep(
462
- deploymentId: string,
463
- name: "clone" | "install" | "build" | "restart" | "healthcheck"
464
- ) {
465
- const stepId = nanoid();
466
- const step: DeploymentStep = {
467
- id: stepId,
468
- deployment_id: deploymentId,
469
- step_name: name,
470
- status: "SUCCESS",
471
- started_at: Date.now(),
472
- finished_at: Date.now(),
473
- duration_ms: 0,
474
- };
475
- createDeploymentStep(step);
476
- appendDeploymentLog(deploymentId, `[Step: ${name}] Skipped (not configured).\n\n`);
477
- }
package/src/github.ts DELETED
@@ -1,124 +0,0 @@
1
- import { Octokit } from "octokit";
2
-
3
- export function getOctokit(token: string): Octokit {
4
- return new Octokit({ auth: token });
5
- }
6
-
7
- export async function validateToken(token: string): Promise<{ username: string; name: string | null }> {
8
- const octokit = getOctokit(token);
9
- const { data } = await octokit.rest.users.getAuthenticated();
10
- return {
11
- username: data.login,
12
- name: data.name || null,
13
- };
14
- }
15
-
16
- export interface GitRepo {
17
- owner: string;
18
- name: string;
19
- fullName: string;
20
- url: string;
21
- }
22
-
23
- export async function listRepositories(token: string): Promise<GitRepo[]> {
24
- const octokit = getOctokit(token);
25
- const repos: GitRepo[] = [];
26
-
27
- // Fetch up to 100 repositories. In a real-world scenario we could paginate further,
28
- // but for an MVP 100 is typically enough or we can list top repos.
29
- const response = await octokit.rest.repos.listForAuthenticatedUser({
30
- per_page: 100,
31
- sort: "updated",
32
- });
33
-
34
- for (const repo of response.data) {
35
- repos.push({
36
- owner: repo.owner.login,
37
- name: repo.name,
38
- fullName: repo.full_name,
39
- url: repo.clone_url,
40
- });
41
- }
42
-
43
- return repos;
44
- }
45
-
46
- export async function listBranches(token: string, owner: string, repo: string): Promise<string[]> {
47
- const octokit = getOctokit(token);
48
- const response = await octokit.rest.repos.listBranches({
49
- owner,
50
- repo,
51
- per_page: 100,
52
- });
53
- return response.data.map(b => b.name);
54
- }
55
-
56
- export interface WebhookConfig {
57
- owner: string;
58
- repo: string;
59
- webhookUrl: string;
60
- secret: string;
61
- }
62
-
63
- export async function setupWebhook(
64
- token: string,
65
- config: WebhookConfig
66
- ): Promise<{ id: number; url: string }> {
67
- const octokit = getOctokit(config.webhookUrl ? token : token); // ensure unused warning bypass
68
-
69
- // 1. List existing webhooks to find a duplicate
70
- const webhooks = await octokit.rest.repos.listWebhooks({
71
- owner: config.owner,
72
- repo: config.repo,
73
- per_page: 100,
74
- });
75
-
76
- const existing = webhooks.data.find(h => h.config.url === config.webhookUrl);
77
-
78
- if (existing) {
79
- // 2. Update webhook
80
- await octokit.rest.repos.updateWebhook({
81
- owner: config.owner,
82
- repo: config.repo,
83
- hook_id: existing.id,
84
- config: {
85
- url: config.webhookUrl,
86
- content_type: "json",
87
- secret: config.secret,
88
- insecure_ssl: "1",
89
- },
90
- events: ["push"],
91
- active: true,
92
- });
93
- return { id: existing.id, url: config.webhookUrl };
94
- } else {
95
- // 3. Create webhook
96
- const response = await octokit.rest.repos.createWebhook({
97
- owner: config.owner,
98
- repo: config.repo,
99
- name: "web",
100
- active: true,
101
- events: ["push"],
102
- config: {
103
- url: config.webhookUrl,
104
- content_type: "json",
105
- secret: config.secret,
106
- insecure_ssl: "1",
107
- },
108
- });
109
- return { id: response.data.id, url: config.webhookUrl };
110
- }
111
- }
112
-
113
- import { execa } from "execa";
114
-
115
- export async function openBrowser(url: string): Promise<void> {
116
- const platform = process.platform;
117
- if (platform === "darwin") {
118
- await execa("open", [url]);
119
- } else if (platform === "win32") {
120
- await execa("cmd", ["/c", "start", url]);
121
- } else {
122
- await execa("xdg-open", [url]);
123
- }
124
- }
package/src/paths.ts DELETED
@@ -1,35 +0,0 @@
1
- import path from "path";
2
- import os from "os";
3
- import fs from "fs";
4
-
5
- export const DEPLOYKIT_DIR = process.env.DEPLOYKIT_DIR || path.join(os.homedir(), ".deploykit");
6
- export const CONFIG_PATH = path.join(DEPLOYKIT_DIR, "config.json");
7
- export const DB_PATH = path.join(DEPLOYKIT_DIR, "deploykit.db");
8
- export const BUILDS_DIR = path.join(DEPLOYKIT_DIR, "builds");
9
-
10
- export function ensureDirsExist() {
11
- if (!fs.existsSync(DEPLOYKIT_DIR)) {
12
- fs.mkdirSync(DEPLOYKIT_DIR, { recursive: true });
13
- }
14
- if (!fs.existsSync(BUILDS_DIR)) {
15
- fs.mkdirSync(BUILDS_DIR, { recursive: true });
16
- }
17
- }
18
-
19
- export function readAuthConfig(): { github_token?: string; github_username?: string } {
20
- ensureDirsExist();
21
- if (!fs.existsSync(CONFIG_PATH)) {
22
- return {};
23
- }
24
- try {
25
- const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
26
- return JSON.parse(raw);
27
- } catch {
28
- return {};
29
- }
30
- }
31
-
32
- export function writeAuthConfig(config: { github_token: string; github_username?: string }) {
33
- ensureDirsExist();
34
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
35
- }