@akash-chowdhury-24/deployhub 2.0.3 → 2.0.5

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.
@@ -0,0 +1,486 @@
1
+ /**
2
+ * Dockerfile generation based on detected framework / project settings.
3
+ */
4
+
5
+ const GENERATED_HEADER =
6
+ '# Generated by DeployHub — review exposed port and start command before deploying.\n';
7
+
8
+ /**
9
+ * @param {string} projectName
10
+ * @returns {string}
11
+ */
12
+ export function sanitizeDockerProjectName(projectName) {
13
+ return projectName.replace(/[^a-zA-Z0-9_-]/g, '-');
14
+ }
15
+
16
+ /**
17
+ * @param {import('../core/config.js').DeployHubConfig} config
18
+ * @returns {{ framework: string, buildCommand: string|null, buildOutput: string, startCommand: string|null, port: number, projectType: string }}
19
+ */
20
+ export function resolveDockerSettings(config) {
21
+ const projectType = config.projectType || 'frontend';
22
+
23
+ if (projectType === 'both' && config.backend) {
24
+ return {
25
+ framework: config.backend.framework || 'express',
26
+ buildCommand: config.backend.buildCommand ?? null,
27
+ buildOutput: config.backend.buildOutput || '.',
28
+ startCommand: config.backend.startCommand || 'npm start',
29
+ port: config.backend.port || 3000,
30
+ projectType,
31
+ };
32
+ }
33
+
34
+ return {
35
+ framework: config.framework || 'express',
36
+ buildCommand: config.buildCommand ?? null,
37
+ buildOutput: config.buildOutput || 'dist',
38
+ startCommand: config.startCommand || 'npm start',
39
+ port: config.port || 3000,
40
+ projectType,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * @param {import('../core/config.js').DeployHubConfig} config
46
+ * @returns {string}
47
+ */
48
+ export function generateDockerfile(config) {
49
+ const settings = resolveDockerSettings(config);
50
+ const { framework } = settings;
51
+
52
+ const frontendStatic = ['react', 'vue', 'angular', 'svelte', 'astro', 'vanilla'];
53
+ if (frontendStatic.includes(framework)) {
54
+ return generateFrontendStaticDockerfile(settings);
55
+ }
56
+
57
+ switch (framework) {
58
+ case 'nextjs':
59
+ return generateNextjsDockerfile(settings);
60
+ case 'nestjs':
61
+ return generateNestjsDockerfile(settings);
62
+ case 'express':
63
+ case 'fastify':
64
+ case 'koa':
65
+ return generateNodeBackendDockerfile(settings);
66
+ case 'fastapi':
67
+ return generateFastapiDockerfile(settings);
68
+ case 'django':
69
+ return generateDjangoDockerfile(settings);
70
+ case 'flask':
71
+ return generateFlaskDockerfile(settings);
72
+ case 'laravel':
73
+ return generateLaravelDockerfile(settings);
74
+ case 'symfony':
75
+ return generateSymfonyDockerfile(settings);
76
+ case 'spring':
77
+ return generateSpringDockerfile(settings);
78
+ case 'go':
79
+ return generateGoDockerfile(settings);
80
+ case 'dotnet':
81
+ return generateDotnetDockerfile(settings);
82
+ case 'rails':
83
+ return generateRailsDockerfile(settings);
84
+ default:
85
+ return generateNodeBackendDockerfile(settings);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {{ buildCommand: string|null, buildOutput: string, port: number }} settings
91
+ */
92
+ function generateFrontendStaticDockerfile(settings) {
93
+ const buildCmd = settings.buildCommand || 'npm run build';
94
+ const output = settings.buildOutput || 'dist';
95
+
96
+ return `${GENERATED_HEADER}
97
+ FROM node:20-alpine AS build
98
+ WORKDIR /app
99
+ COPY package*.json ./
100
+ RUN npm ci
101
+ COPY . .
102
+ RUN ${buildCmd}
103
+
104
+ FROM nginx:alpine
105
+ COPY --from=build /app/${output} /usr/share/nginx/html
106
+ EXPOSE 80
107
+ CMD ["nginx", "-g", "daemon off;"]
108
+ `;
109
+ }
110
+
111
+ /**
112
+ * @param {{ buildCommand: string|null, startCommand: string|null, port: number }} settings
113
+ */
114
+ function generateNextjsDockerfile(settings) {
115
+ const buildCmd = settings.buildCommand || 'npm run build';
116
+ const startCmd = settings.startCommand || 'npm start';
117
+ const port = settings.port || 3000;
118
+ const startParts = parseCommand(startCmd);
119
+
120
+ return `${GENERATED_HEADER}
121
+ FROM node:20-alpine AS deps
122
+ WORKDIR /app
123
+ COPY package*.json ./
124
+ RUN npm ci
125
+
126
+ FROM node:20-alpine AS build
127
+ WORKDIR /app
128
+ COPY --from=deps /app/node_modules ./node_modules
129
+ COPY . .
130
+ RUN ${buildCmd}
131
+
132
+ FROM node:20-alpine AS runner
133
+ WORKDIR /app
134
+ ENV NODE_ENV=production
135
+ COPY --from=build /app/package*.json ./
136
+ COPY --from=build /app/node_modules ./node_modules
137
+ COPY --from=build /app/.next ./.next
138
+ COPY --from=build /app/public ./public
139
+ EXPOSE ${port}
140
+ CMD ${JSON.stringify(startParts)}
141
+ `;
142
+ }
143
+
144
+ /**
145
+ * @param {{ buildCommand: string|null, buildOutput: string, startCommand: string|null, port: number }} settings
146
+ */
147
+ function generateNestjsDockerfile(settings) {
148
+ const buildCmd = settings.buildCommand || 'npm run build';
149
+ const startCmd = settings.startCommand || 'node dist/main';
150
+ const port = settings.port || 3000;
151
+ const startParts = parseCommand(startCmd);
152
+
153
+ return `${GENERATED_HEADER}
154
+ FROM node:20-alpine AS build
155
+ WORKDIR /app
156
+ COPY package*.json ./
157
+ RUN npm ci
158
+ COPY . .
159
+ RUN ${buildCmd}
160
+
161
+ FROM node:20-alpine
162
+ WORKDIR /app
163
+ ENV NODE_ENV=production
164
+ COPY package*.json ./
165
+ RUN npm ci --omit=dev
166
+ COPY --from=build /app/${settings.buildOutput || 'dist'} ./${settings.buildOutput || 'dist'}
167
+ EXPOSE ${port}
168
+ CMD ${JSON.stringify(startParts)}
169
+ `;
170
+ }
171
+
172
+ /**
173
+ * @param {{ buildCommand: string|null, buildOutput: string, startCommand: string|null, port: number }} settings
174
+ */
175
+ function generateNodeBackendDockerfile(settings) {
176
+ const port = settings.port || 3000;
177
+ const startCmd = settings.startCommand || 'npm start';
178
+ const startParts = parseCommand(startCmd);
179
+
180
+ if (settings.buildCommand) {
181
+ return `${GENERATED_HEADER}
182
+ FROM node:20-alpine AS build
183
+ WORKDIR /app
184
+ COPY package*.json ./
185
+ RUN npm ci
186
+ COPY . .
187
+ RUN ${settings.buildCommand}
188
+
189
+ FROM node:20-alpine
190
+ WORKDIR /app
191
+ ENV NODE_ENV=production
192
+ COPY package*.json ./
193
+ RUN npm ci --omit=dev
194
+ COPY --from=build /app/${settings.buildOutput || '.'} ./${settings.buildOutput || '.'}
195
+ EXPOSE ${port}
196
+ CMD ${JSON.stringify(startParts)}
197
+ `;
198
+ }
199
+
200
+ return `${GENERATED_HEADER}
201
+ FROM node:20-alpine AS deps
202
+ WORKDIR /app
203
+ COPY package*.json ./
204
+ RUN npm ci --omit=dev
205
+
206
+ FROM node:20-alpine
207
+ WORKDIR /app
208
+ ENV NODE_ENV=production
209
+ COPY --from=deps /app/node_modules ./node_modules
210
+ COPY . .
211
+ EXPOSE ${port}
212
+ CMD ${JSON.stringify(startParts)}
213
+ `;
214
+ }
215
+
216
+ /**
217
+ * @param {{ startCommand: string|null, port: number }} settings
218
+ */
219
+ function generateFastapiDockerfile(settings) {
220
+ const port = settings.port || 8000;
221
+ const startCmd = settings.startCommand || `uvicorn main:app --host 0.0.0.0 --port ${port}`;
222
+ const startParts = parseCommand(startCmd);
223
+
224
+ return `${GENERATED_HEADER}
225
+ FROM python:3.11-slim AS build
226
+ WORKDIR /app
227
+ COPY requirements.txt ./
228
+ RUN pip install --no-cache-dir -r requirements.txt
229
+
230
+ FROM python:3.11-slim
231
+ WORKDIR /app
232
+ COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
233
+ COPY --from=build /usr/local/bin /usr/local/bin
234
+ COPY . .
235
+ EXPOSE ${port}
236
+ CMD ${JSON.stringify(startParts)}
237
+ `;
238
+ }
239
+
240
+ /**
241
+ * @param {{ startCommand: string|null, port: number }} settings
242
+ */
243
+ function generateDjangoDockerfile(settings) {
244
+ const port = settings.port || 8000;
245
+ const startCmd =
246
+ settings.startCommand || `gunicorn config.wsgi:application --bind 0.0.0.0:${port}`;
247
+ const startParts = parseCommand(startCmd);
248
+
249
+ return `${GENERATED_HEADER}
250
+ FROM python:3.11-slim AS build
251
+ WORKDIR /app
252
+ COPY requirements.txt ./
253
+ RUN pip install --no-cache-dir -r requirements.txt
254
+
255
+ FROM python:3.11-slim
256
+ WORKDIR /app
257
+ COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
258
+ COPY --from=build /usr/local/bin /usr/local/bin
259
+ COPY . .
260
+ EXPOSE ${port}
261
+ CMD ${JSON.stringify(startParts)}
262
+ `;
263
+ }
264
+
265
+ /**
266
+ * @param {{ startCommand: string|null, port: number }} settings
267
+ */
268
+ function generateFlaskDockerfile(settings) {
269
+ const port = settings.port || 5000;
270
+ const startCmd = settings.startCommand || `gunicorn app:app --bind 0.0.0.0:${port}`;
271
+ const startParts = parseCommand(startCmd);
272
+
273
+ return `${GENERATED_HEADER}
274
+ FROM python:3.11-slim AS build
275
+ WORKDIR /app
276
+ COPY requirements.txt ./
277
+ RUN pip install --no-cache-dir -r requirements.txt
278
+
279
+ FROM python:3.11-slim
280
+ WORKDIR /app
281
+ COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
282
+ COPY --from=build /usr/local/bin /usr/local/bin
283
+ COPY . .
284
+ EXPOSE ${port}
285
+ CMD ${JSON.stringify(startParts)}
286
+ `;
287
+ }
288
+
289
+ /**
290
+ * @param {{ port: number }} settings
291
+ */
292
+ function generateLaravelDockerfile(settings) {
293
+ const port = settings.port || 80;
294
+
295
+ return `${GENERATED_HEADER}
296
+ FROM composer:2 AS vendor
297
+ WORKDIR /app
298
+ COPY composer.json composer.lock ./
299
+ RUN composer install --no-dev --optimize-autoloader --no-interaction
300
+
301
+ FROM php:8.2-fpm-alpine
302
+ WORKDIR /var/www/html
303
+ COPY --from=vendor /app/vendor ./vendor
304
+ COPY . .
305
+ RUN chown -R www-data:www-data storage bootstrap/cache || true
306
+ EXPOSE ${port}
307
+ CMD ["php-fpm"]
308
+ `;
309
+ }
310
+
311
+ /**
312
+ * @param {{ port: number }} settings
313
+ */
314
+ function generateSymfonyDockerfile(settings) {
315
+ const port = settings.port || 80;
316
+
317
+ return `${GENERATED_HEADER}
318
+ FROM composer:2 AS vendor
319
+ WORKDIR /app
320
+ COPY composer.json composer.lock ./
321
+ RUN composer install --no-dev --optimize-autoloader --no-interaction
322
+
323
+ FROM php:8.2-fpm-alpine
324
+ WORKDIR /var/www/html
325
+ COPY --from=vendor /app/vendor ./vendor
326
+ COPY . .
327
+ EXPOSE ${port}
328
+ CMD ["php-fpm"]
329
+ `;
330
+ }
331
+
332
+ /**
333
+ * @param {{ buildCommand: string|null, port: number }} settings
334
+ */
335
+ function generateSpringDockerfile(settings) {
336
+ const buildCmd = settings.buildCommand || 'mvn package -DskipTests';
337
+ const port = settings.port || 8080;
338
+
339
+ return `${GENERATED_HEADER}
340
+ FROM eclipse-temurin:17-jdk-alpine AS build
341
+ WORKDIR /app
342
+ COPY pom.xml ./
343
+ COPY src ./src
344
+ RUN apk add --no-cache maven && ${buildCmd}
345
+
346
+ FROM eclipse-temurin:17-jre-alpine
347
+ WORKDIR /app
348
+ COPY --from=build /app/target/*.jar app.jar
349
+ EXPOSE ${port}
350
+ CMD ["java", "-jar", "app.jar"]
351
+ `;
352
+ }
353
+
354
+ /**
355
+ * @param {{ buildCommand: string|null, startCommand: string|null, port: number }} settings
356
+ */
357
+ function generateGoDockerfile(settings) {
358
+ const buildCmd = settings.buildCommand || 'go build -o /app/bin/app .';
359
+ const port = settings.port || 8080;
360
+
361
+ return `${GENERATED_HEADER}
362
+ FROM golang:1.22-alpine AS build
363
+ WORKDIR /app
364
+ COPY go.mod go.sum ./
365
+ RUN go mod download
366
+ COPY . .
367
+ RUN ${buildCmd}
368
+
369
+ FROM alpine:3.19
370
+ WORKDIR /app
371
+ RUN apk add --no-cache ca-certificates
372
+ COPY --from=build /app/bin/app ./app
373
+ EXPOSE ${port}
374
+ CMD ["./app"]
375
+ `;
376
+ }
377
+
378
+ /**
379
+ * @param {{ buildCommand: string|null, buildOutput: string, startCommand: string|null, port: number }} settings
380
+ */
381
+ function generateDotnetDockerfile(settings) {
382
+ const buildCmd = settings.buildCommand || 'dotnet publish -c Release -o /app/publish';
383
+ const port = settings.port || 5000;
384
+ const output = settings.buildOutput || 'publish';
385
+
386
+ return `${GENERATED_HEADER}
387
+ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
388
+ WORKDIR /src
389
+ COPY *.csproj ./
390
+ RUN dotnet restore
391
+ COPY . .
392
+ RUN ${buildCmd}
393
+
394
+ FROM mcr.microsoft.com/dotnet/aspnet:8.0
395
+ WORKDIR /app
396
+ COPY --from=build /src/${output} .
397
+ EXPOSE ${port}
398
+ ENV ASPNETCORE_URLS=http://+:${port}
399
+ CMD ["dotnet", "App.dll"]
400
+ `;
401
+ }
402
+
403
+ /**
404
+ * @param {{ startCommand: string|null, port: number }} settings
405
+ */
406
+ function generateRailsDockerfile(settings) {
407
+ const port = settings.port || 3000;
408
+ const startCmd = settings.startCommand || 'bundle exec puma -C config/puma.rb';
409
+ const startParts = parseCommand(startCmd);
410
+
411
+ return `${GENERATED_HEADER}
412
+ FROM ruby:3.2-slim AS build
413
+ WORKDIR /app
414
+ RUN apt-get update -qq && apt-get install -y build-essential libpq-dev && rm -rf /var/lib/apt/lists/*
415
+ COPY Gemfile Gemfile.lock ./
416
+ RUN bundle install --without development test
417
+ COPY . .
418
+ RUN bundle exec rake assets:precompile || true
419
+
420
+ FROM ruby:3.2-slim
421
+ WORKDIR /app
422
+ RUN apt-get update -qq && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
423
+ COPY --from=build /usr/local/bundle /usr/local/bundle
424
+ COPY --from=build /app .
425
+ EXPOSE ${port}
426
+ CMD ${JSON.stringify(startParts)}
427
+ `;
428
+ }
429
+
430
+ /**
431
+ * @param {string} command
432
+ * @returns {string[]}
433
+ */
434
+ function parseCommand(command) {
435
+ const trimmed = command.trim();
436
+ if (trimmed.startsWith('[')) {
437
+ try {
438
+ return JSON.parse(trimmed);
439
+ } catch {
440
+ // fall through
441
+ }
442
+ }
443
+
444
+ const match = trimmed.match(/^(\S+)(?:\s+(.*))?$/);
445
+ if (!match) return ['sh', '-c', trimmed];
446
+ const [, bin, rest] = match;
447
+ if (!rest) return [bin];
448
+ return [bin, ...rest.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g).map((part) => part.replace(/^['"]|['"]$/g, ''))];
449
+ }
450
+
451
+ /**
452
+ * Human-readable framework label for user messages.
453
+ * @param {string} framework
454
+ */
455
+ export function getDockerfileFrameworkLabel(framework) {
456
+ const labels = {
457
+ react: 'React',
458
+ vue: 'Vue',
459
+ angular: 'Angular',
460
+ nextjs: 'Next.js',
461
+ svelte: 'Svelte',
462
+ astro: 'Astro',
463
+ vanilla: 'Vanilla JS',
464
+ express: 'Express',
465
+ nestjs: 'NestJS',
466
+ fastify: 'Fastify',
467
+ koa: 'Koa',
468
+ fastapi: 'FastAPI',
469
+ django: 'Django',
470
+ flask: 'Flask',
471
+ laravel: 'Laravel',
472
+ symfony: 'Symfony',
473
+ spring: 'Spring Boot',
474
+ go: 'Go',
475
+ dotnet: '.NET',
476
+ rails: 'Ruby on Rails',
477
+ };
478
+ return labels[framework] || framework;
479
+ }
480
+
481
+ export default {
482
+ generateDockerfile,
483
+ resolveDockerSettings,
484
+ sanitizeDockerProjectName,
485
+ getDockerfileFrameworkLabel,
486
+ };
@@ -208,6 +208,56 @@ function getBackendSetupSteps(config) {
208
208
  return steps;
209
209
  }
210
210
 
211
+ const KUBECTL_VERSION = 'v1.30.4';
212
+
213
+ /**
214
+ * @param {string[]} deployEnvironments
215
+ * @param {Record<string, { type: string }>} environments
216
+ * @returns {boolean}
217
+ */
218
+ function hasKubernetesDeploy(deployEnvironments, environments) {
219
+ return deployEnvironments.some((envName) => environments[envName]?.type === 'kubernetes');
220
+ }
221
+
222
+ /**
223
+ * @returns {string}
224
+ */
225
+ function getKubernetesSetupSteps() {
226
+ return ` - name: Setup kubectl
227
+ uses: azure/setup-kubectl@v4
228
+ with:
229
+ version: '${KUBECTL_VERSION}'
230
+
231
+ - name: Configure kubeconfig
232
+ env:
233
+ KUBECONFIG_SECRET: \${{ secrets.KUBECONFIG }}
234
+ run: |
235
+ mkdir -p "$GITHUB_WORKSPACE/.kube"
236
+ if echo "$KUBECONFIG_SECRET" | base64 -d > "$GITHUB_WORKSPACE/.kube/config" 2>/dev/null; then
237
+ :
238
+ else
239
+ printf '%s' "$KUBECONFIG_SECRET" > "$GITHUB_WORKSPACE/.kube/config"
240
+ fi
241
+ chmod 600 "$GITHUB_WORKSPACE/.kube/config"`;
242
+ }
243
+
244
+ /**
245
+ * @param {string[]} deployEnvironments
246
+ * @param {Record<string, { type: string }>} environments
247
+ * @param {Set<string>} envVars
248
+ */
249
+ function applyKubernetesWorkflowEnv(deployEnvironments, environments, envVars) {
250
+ if (!hasKubernetesDeploy(deployEnvironments, environments)) return;
251
+
252
+ for (const entry of envVars) {
253
+ if (entry.startsWith('KUBECONFIG:')) {
254
+ envVars.delete(entry);
255
+ break;
256
+ }
257
+ }
258
+ envVars.add('KUBECONFIG: ${{ github.workspace }}/.kube/config');
259
+ }
260
+
211
261
  /**
212
262
  * @param {string[]} storageProviders
213
263
  * @param {string[]} deployEnvironments
@@ -243,6 +293,8 @@ export function generateWorkflowYaml(
243
293
  }
244
294
  }
245
295
 
296
+ applyKubernetesWorkflowEnv(deployEnvironments, environments, envVars);
297
+
246
298
  const envBlock = Array.from(envVars)
247
299
  .map((line) => ` ${line}`)
248
300
  .join('\n');
@@ -253,6 +305,9 @@ export function generateWorkflowYaml(
253
305
  const githubGitConfigStep = isGithubCliSource(cliSource)
254
306
  ? `${getGithubGitConfigStep()}\n`
255
307
  : '';
308
+ const kubernetesSteps = hasKubernetesDeploy(deployEnvironments, environments)
309
+ ? `${getKubernetesSetupSteps()}\n`
310
+ : '';
256
311
 
257
312
  const projectType = config?.projectType || 'frontend';
258
313
  const installDepsCommand =
@@ -272,7 +327,7 @@ jobs:
272
327
  ${uniqueBackendSteps ? `${uniqueBackendSteps}\n` : ''} - uses: actions/setup-node@v4
273
328
  with:
274
329
  node-version: '20'
275
- ${githubGitConfigStep} - name: Install project dependencies
330
+ ${kubernetesSteps}${githubGitConfigStep} - name: Install project dependencies
276
331
  run: ${installDepsCommand}
277
332
  - name: Install DeployHub CLI
278
333
  run: npm install ${installSpec} --no-save