@akash-chowdhury-24/deployhub 1.0.7 → 1.0.9
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/package.json +1 -1
- package/src/commands/init.js +21 -21
- package/src/utils/github-actions.js +207 -4
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -14,8 +14,11 @@ import { getProjectVersion } from '../utils/version.js';
|
|
|
14
14
|
import {
|
|
15
15
|
writeWorkflowFile,
|
|
16
16
|
getRequiredSecrets,
|
|
17
|
+
generateEnvExampleContent,
|
|
17
18
|
guessCliGithubRepo,
|
|
18
19
|
addDeployhubToPackageJson,
|
|
20
|
+
isGithubCliSource,
|
|
21
|
+
normalizeGithubCliSource,
|
|
19
22
|
} from '../utils/github-actions.js';
|
|
20
23
|
import {
|
|
21
24
|
promptPlatformQuestions,
|
|
@@ -288,25 +291,6 @@ function buildServerEnvEntry(
|
|
|
288
291
|
return envEntry;
|
|
289
292
|
}
|
|
290
293
|
|
|
291
|
-
/**
|
|
292
|
-
* @returns {Promise<string>}
|
|
293
|
-
*/
|
|
294
|
-
async function getEnvExampleContent() {
|
|
295
|
-
const possiblePaths = [
|
|
296
|
-
path.join(path.dirname(process.execPath), '.env.example'),
|
|
297
|
-
path.join(process.cwd(), '.env.example'),
|
|
298
|
-
new URL('../../.env.example', import.meta.url).pathname,
|
|
299
|
-
];
|
|
300
|
-
for (const p of possiblePaths) {
|
|
301
|
-
try {
|
|
302
|
-
return await fs.readFile(p, 'utf-8');
|
|
303
|
-
} catch {
|
|
304
|
-
// try next path
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return '# Add your environment variables here\n';
|
|
308
|
-
}
|
|
309
|
-
|
|
310
294
|
/**
|
|
311
295
|
* @param {Record<string, unknown>} config
|
|
312
296
|
* @param {Record<string, Record<string, unknown>>} environments
|
|
@@ -470,6 +454,7 @@ export function registerInitCommand(program) {
|
|
|
470
454
|
message:
|
|
471
455
|
'DeployHub CLI source for GitHub Actions (github:user/repo or npm:@akash-chowdhury-24/deployhub):',
|
|
472
456
|
default: defaultCliRepo,
|
|
457
|
+
filter: (value) => normalizeGithubCliSource(value),
|
|
473
458
|
},
|
|
474
459
|
]);
|
|
475
460
|
|
|
@@ -780,7 +765,12 @@ export function registerInitCommand(program) {
|
|
|
780
765
|
await generateProjectScaffold(config, environments, cwd);
|
|
781
766
|
|
|
782
767
|
const envExampleDest = path.join(cwd, '.env.example');
|
|
783
|
-
const envExampleContent =
|
|
768
|
+
const envExampleContent = generateEnvExampleContent(
|
|
769
|
+
config.storage,
|
|
770
|
+
deploy,
|
|
771
|
+
environments,
|
|
772
|
+
config
|
|
773
|
+
);
|
|
784
774
|
await fs.writeFile(envExampleDest, envExampleContent);
|
|
785
775
|
|
|
786
776
|
const secrets = getRequiredSecrets(config.storage, deploy, environments, config);
|
|
@@ -794,7 +784,17 @@ export function registerInitCommand(program) {
|
|
|
794
784
|
console.log(' • .env.example');
|
|
795
785
|
console.log('');
|
|
796
786
|
console.log(chalk.bold('Next steps:'));
|
|
797
|
-
|
|
787
|
+
if (isGithubCliSource(answers.cliSource)) {
|
|
788
|
+
console.log(
|
|
789
|
+
' 1. For a private DeployHub CLI repo, create a GitHub PAT with repo read access'
|
|
790
|
+
);
|
|
791
|
+
console.log(
|
|
792
|
+
` and add it as ${chalk.cyan('DEPLOYHUB_GITHUB_TOKEN')} in your demo project secrets`
|
|
793
|
+
);
|
|
794
|
+
console.log(' (public CLI repos can skip this — HTTPS is used automatically)');
|
|
795
|
+
} else {
|
|
796
|
+
console.log(' 1. Push the DeployHub CLI repo to GitHub (if using github: source)');
|
|
797
|
+
}
|
|
798
798
|
console.log(' 2. Copy .env.example to .env and fill in credentials');
|
|
799
799
|
if (secrets.length > 0) {
|
|
800
800
|
console.log(' 3. Add these secrets to GitHub (Settings → Secrets):');
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
PLATFORM_ENV_MAP,
|
|
5
|
+
PLATFORM_CLI_MAP,
|
|
6
|
+
PLATFORM_CHOICES,
|
|
7
|
+
getPlatformEnvExample,
|
|
8
|
+
} from './platform-env.js';
|
|
4
9
|
import { getWorkflowHeaderComment } from './author.js';
|
|
5
10
|
|
|
6
11
|
/** @typedef {'aws'|'azure'|'gcp'|'gdrive'|'dropbox'|'local'|'ftp'|'ssh'} ProviderEnvKey */
|
|
@@ -32,8 +37,68 @@ const BACKEND_SSH_ENV_VARS = [
|
|
|
32
37
|
'SSH_PORT',
|
|
33
38
|
];
|
|
34
39
|
|
|
40
|
+
const PROVIDER_LABELS = {
|
|
41
|
+
aws: 'AWS S3',
|
|
42
|
+
azure: 'Azure Blob',
|
|
43
|
+
gcp: 'GCP',
|
|
44
|
+
gdrive: 'Google Drive',
|
|
45
|
+
dropbox: 'Dropbox',
|
|
46
|
+
ftp: 'FTP',
|
|
47
|
+
ssh: 'SSH Deployment',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const PLATFORM_LABELS = Object.fromEntries(
|
|
51
|
+
PLATFORM_CHOICES.map(({ name, value }) => [value, name])
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const ENV_VAR_DEFAULTS = {
|
|
55
|
+
AWS_REGION: 'us-east-1',
|
|
56
|
+
FTP_PORT: '21',
|
|
57
|
+
FTP_PATH: '/uploads',
|
|
58
|
+
SSH_DEPLOY_PATH: '/var/www/app',
|
|
59
|
+
SMTP_PORT: '587',
|
|
60
|
+
};
|
|
61
|
+
|
|
35
62
|
const NPM_PACKAGE = '@akash-chowdhury-24/deployhub';
|
|
36
63
|
const DEFAULT_NPM_CLI_SOURCE = `npm:${NPM_PACKAGE}`;
|
|
64
|
+
export const GITHUB_CLI_TOKEN_SECRET = 'DEPLOYHUB_GITHUB_TOKEN';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} [cliSource]
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
export function isGithubCliSource(cliSource) {
|
|
71
|
+
const normalized = normalizeCliSource(cliSource);
|
|
72
|
+
return normalized.startsWith('github:');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Normalizes common GitHub remote formats to github:user/repo.
|
|
77
|
+
* @param {string} cliSource
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export function normalizeGithubCliSource(cliSource) {
|
|
81
|
+
if (!cliSource) return cliSource;
|
|
82
|
+
|
|
83
|
+
const trimmed = cliSource.trim();
|
|
84
|
+
const httpsMatch = trimmed.match(
|
|
85
|
+
/^https?:\/\/github\.com\/([^/]+)\/([^/#?]+?)(?:\.git)?(?:[/?#].*)?$/
|
|
86
|
+
);
|
|
87
|
+
if (httpsMatch) {
|
|
88
|
+
return `github:${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/#?]+?)(?:\.git)?$/);
|
|
92
|
+
if (sshMatch) {
|
|
93
|
+
return `github:${sshMatch[1]}/${sshMatch[2]}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (trimmed.startsWith('github:')) {
|
|
97
|
+
return trimmed.replace(/^github:/, 'github:').replace(/\.git$/, '');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return trimmed;
|
|
101
|
+
}
|
|
37
102
|
|
|
38
103
|
/** @param {string} [cliSource] */
|
|
39
104
|
function normalizeCliSource(cliSource) {
|
|
@@ -41,7 +106,7 @@ function normalizeCliSource(cliSource) {
|
|
|
41
106
|
if (/^npm:(deployhub-cli|deploy-hub-cli|deployhub)$/.test(cliSource)) {
|
|
42
107
|
return DEFAULT_NPM_CLI_SOURCE;
|
|
43
108
|
}
|
|
44
|
-
return cliSource;
|
|
109
|
+
return normalizeGithubCliSource(cliSource);
|
|
45
110
|
}
|
|
46
111
|
|
|
47
112
|
/**
|
|
@@ -62,6 +127,26 @@ export function getCliInstallSpec(cliSource) {
|
|
|
62
127
|
return normalized;
|
|
63
128
|
}
|
|
64
129
|
|
|
130
|
+
/**
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
function getGithubGitConfigStep() {
|
|
134
|
+
return ` - name: Configure GitHub access for DeployHub CLI
|
|
135
|
+
env:
|
|
136
|
+
${GITHUB_CLI_TOKEN_SECRET}: \${{ secrets.${GITHUB_CLI_TOKEN_SECRET} }}
|
|
137
|
+
GITHUB_TOKEN: \${{ github.token }}
|
|
138
|
+
run: |
|
|
139
|
+
TOKEN="\${${GITHUB_CLI_TOKEN_SECRET}:-$GITHUB_TOKEN}"
|
|
140
|
+
if [ -n "$TOKEN" ]; then
|
|
141
|
+
git config --global url."https://oauth2:\${TOKEN}@github.com/".insteadOf "https://github.com/"
|
|
142
|
+
git config --global url."https://oauth2:\${TOKEN}@github.com/".insteadOf "git@github.com:"
|
|
143
|
+
git config --global url."https://oauth2:\${TOKEN}@github.com/".insteadOf "ssh://git@github.com/"
|
|
144
|
+
else
|
|
145
|
+
git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
|
|
146
|
+
git config --global url."https://github.com/".insteadOf "git@github.com:"
|
|
147
|
+
fi`;
|
|
148
|
+
}
|
|
149
|
+
|
|
65
150
|
/**
|
|
66
151
|
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
67
152
|
* @returns {string[]}
|
|
@@ -219,6 +304,9 @@ export function generateWorkflowYaml(
|
|
|
219
304
|
const installSpec = getCliInstallSpec(cliSource);
|
|
220
305
|
const backendSteps = getBackendSetupSteps(config);
|
|
221
306
|
const uniqueBackendSteps = [...new Set(backendSteps)].join('\n');
|
|
307
|
+
const githubGitConfigStep = isGithubCliSource(cliSource)
|
|
308
|
+
? `${getGithubGitConfigStep()}\n`
|
|
309
|
+
: '';
|
|
222
310
|
|
|
223
311
|
const envList = deployEnvironments
|
|
224
312
|
.map((name) => environments[name])
|
|
@@ -243,7 +331,7 @@ jobs:
|
|
|
243
331
|
${uniqueBackendSteps ? `${uniqueBackendSteps}\n` : ''} - uses: actions/setup-node@v4
|
|
244
332
|
with:
|
|
245
333
|
node-version: '20'
|
|
246
|
-
- name: Install project dependencies
|
|
334
|
+
${githubGitConfigStep} - name: Install project dependencies
|
|
247
335
|
run: ${installDepsCommand}
|
|
248
336
|
${platformSteps ? `${platformSteps}\n` : ''} - name: Install DeployHub CLI
|
|
249
337
|
run: npm install ${installSpec} --no-save
|
|
@@ -364,11 +452,17 @@ export function getRequiredSecrets(
|
|
|
364
452
|
storageProviders,
|
|
365
453
|
deployEnvironments,
|
|
366
454
|
environments,
|
|
367
|
-
config = null
|
|
455
|
+
config = null,
|
|
456
|
+
cliSource = null
|
|
368
457
|
) {
|
|
369
458
|
/** @type {Set<string>} */
|
|
370
459
|
const secrets = new Set();
|
|
371
460
|
|
|
461
|
+
const resolvedCliSource = cliSource || config?.cli?.source;
|
|
462
|
+
if (isGithubCliSource(resolvedCliSource)) {
|
|
463
|
+
secrets.add(GITHUB_CLI_TOKEN_SECRET);
|
|
464
|
+
}
|
|
465
|
+
|
|
372
466
|
for (const provider of storageProviders) {
|
|
373
467
|
const keys = PROVIDER_ENV_MAP[provider] || [];
|
|
374
468
|
keys.forEach((k) => secrets.add(k));
|
|
@@ -400,4 +494,113 @@ export function getRequiredSecrets(
|
|
|
400
494
|
return Array.from(secrets);
|
|
401
495
|
}
|
|
402
496
|
|
|
497
|
+
/**
|
|
498
|
+
* @param {string} title
|
|
499
|
+
* @param {string[]} keys
|
|
500
|
+
* @param {Record<string, string>} [defaults]
|
|
501
|
+
* @param {Set<string>} seenKeys
|
|
502
|
+
* @returns {string}
|
|
503
|
+
*/
|
|
504
|
+
function formatEnvSection(title, keys, defaults, seenKeys) {
|
|
505
|
+
const newKeys = keys.filter((key) => !seenKeys.has(key));
|
|
506
|
+
if (newKeys.length === 0) return '';
|
|
507
|
+
|
|
508
|
+
for (const key of newKeys) {
|
|
509
|
+
seenKeys.add(key);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const lines = newKeys.map((key) => {
|
|
513
|
+
const value = defaults?.[key] ?? ENV_VAR_DEFAULTS[key] ?? '';
|
|
514
|
+
return value ? `${key}=${value}` : `${key}=`;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
return `# ${title}\n${lines.join('\n')}\n`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @param {string[]} storageProviders
|
|
522
|
+
* @param {string[]} deployEnvironments
|
|
523
|
+
* @param {Record<string, Record<string, unknown>>} environments
|
|
524
|
+
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
525
|
+
* @returns {string}
|
|
526
|
+
*/
|
|
527
|
+
export function generateEnvExampleContent(
|
|
528
|
+
storageProviders,
|
|
529
|
+
deployEnvironments,
|
|
530
|
+
environments,
|
|
531
|
+
config = null
|
|
532
|
+
) {
|
|
533
|
+
/** @type {Set<string>} */
|
|
534
|
+
const seenKeys = new Set();
|
|
535
|
+
/** @type {string[]} */
|
|
536
|
+
const sections = [];
|
|
537
|
+
|
|
538
|
+
const addSection = (title, keys, defaults = {}) => {
|
|
539
|
+
const section = formatEnvSection(title, keys, defaults, seenKeys);
|
|
540
|
+
if (section) sections.push(section);
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
for (const provider of storageProviders) {
|
|
544
|
+
const keys = PROVIDER_ENV_MAP[provider] || [];
|
|
545
|
+
if (keys.length > 0) {
|
|
546
|
+
addSection(PROVIDER_LABELS[provider] || provider, keys);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
for (const envName of deployEnvironments) {
|
|
551
|
+
const env = environments[envName];
|
|
552
|
+
if (!env) continue;
|
|
553
|
+
|
|
554
|
+
if (env.deploymentType === 'platform' || env.frontendDeploymentType === 'platform') {
|
|
555
|
+
const platform = env.platform;
|
|
556
|
+
if (platform) {
|
|
557
|
+
const examples = getPlatformEnvExample(platform);
|
|
558
|
+
addSection(
|
|
559
|
+
PLATFORM_LABELS[platform] || platform,
|
|
560
|
+
Object.keys(examples),
|
|
561
|
+
examples
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const keys = PROVIDER_ENV_MAP[env.type] || [];
|
|
568
|
+
if (keys.length > 0) {
|
|
569
|
+
addSection(PROVIDER_LABELS[env.type] || env.type, keys);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (env.type === 'ssh' && config) {
|
|
573
|
+
const projectType = config.projectType || 'frontend';
|
|
574
|
+
if (projectType === 'backend' || projectType === 'both') {
|
|
575
|
+
addSection('SSH Deployment (backend)', BACKEND_SSH_ENV_VARS);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (config?.notifications) {
|
|
581
|
+
if (config.notifications.slack) {
|
|
582
|
+
addSection('Notifications', ['SLACK_WEBHOOK_URL']);
|
|
583
|
+
}
|
|
584
|
+
if (config.notifications.webhook) {
|
|
585
|
+
addSection('Notifications', ['WEBHOOK_URL']);
|
|
586
|
+
}
|
|
587
|
+
if (config.notifications.email) {
|
|
588
|
+
addSection('Email (SMTP)', [
|
|
589
|
+
'SMTP_HOST',
|
|
590
|
+
'SMTP_PORT',
|
|
591
|
+
'SMTP_USER',
|
|
592
|
+
'SMTP_PASS',
|
|
593
|
+
'NOTIFICATION_EMAIL',
|
|
594
|
+
'NOTIFY_EMAIL_TO',
|
|
595
|
+
]);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (sections.length === 0) {
|
|
600
|
+
return '# Add your environment variables here\n';
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return `${sections.join('\n')}\n`;
|
|
604
|
+
}
|
|
605
|
+
|
|
403
606
|
export { PROVIDER_ENV_MAP };
|