@akash-chowdhury-24/deployhub 1.0.0
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/LICENSE +21 -0
- package/README.md +176 -0
- package/install.ps1 +55 -0
- package/install.sh +99 -0
- package/package.json +86 -0
- package/src/adapters/dotnet.adapter.js +41 -0
- package/src/adapters/go.adapter.js +40 -0
- package/src/adapters/index.js +48 -0
- package/src/adapters/java.adapter.js +46 -0
- package/src/adapters/node.adapter.js +77 -0
- package/src/adapters/php.adapter.js +43 -0
- package/src/adapters/python.adapter.js +54 -0
- package/src/adapters/rails.adapter.js +66 -0
- package/src/artifact/engine.js +473 -0
- package/src/cli/index.js +45 -0
- package/src/commands/artifact.js +88 -0
- package/src/commands/build.js +44 -0
- package/src/commands/clean.js +50 -0
- package/src/commands/deploy.js +82 -0
- package/src/commands/doctor.js +630 -0
- package/src/commands/init.js +795 -0
- package/src/commands/logs.js +33 -0
- package/src/commands/rollback.js +50 -0
- package/src/commands/storage.js +116 -0
- package/src/commands/update.js +63 -0
- package/src/commands/verify.js +55 -0
- package/src/core/config.js +168 -0
- package/src/core/pipeline.js +77 -0
- package/src/core/stages.js +210 -0
- package/src/deployment/index.js +156 -0
- package/src/deployment/providers/azure-vm.js +7 -0
- package/src/deployment/providers/docker.js +30 -0
- package/src/deployment/providers/ec2.js +7 -0
- package/src/deployment/providers/gcp-vm.js +7 -0
- package/src/deployment/providers/kubernetes.js +30 -0
- package/src/deployment/providers/platforms/_shared.js +167 -0
- package/src/deployment/providers/platforms/aws-amplify.js +164 -0
- package/src/deployment/providers/platforms/azure-static-web-apps.js +68 -0
- package/src/deployment/providers/platforms/cloudflare-pages.js +103 -0
- package/src/deployment/providers/platforms/firebase-app-hosting.js +95 -0
- package/src/deployment/providers/platforms/firebase-hosting.js +99 -0
- package/src/deployment/providers/platforms/index.js +44 -0
- package/src/deployment/providers/platforms/netlify.js +102 -0
- package/src/deployment/providers/platforms/vercel.js +92 -0
- package/src/deployment/providers/ssh.js +365 -0
- package/src/detectors/angular.js +23 -0
- package/src/detectors/backend.detector.js +304 -0
- package/src/detectors/dotnet.js +18 -0
- package/src/detectors/frontend.detector.js +219 -0
- package/src/detectors/go.js +20 -0
- package/src/detectors/index.js +78 -0
- package/src/detectors/java.js +24 -0
- package/src/detectors/nextjs.js +23 -0
- package/src/detectors/node.js +28 -0
- package/src/detectors/php.js +17 -0
- package/src/detectors/python.js +22 -0
- package/src/detectors/react.js +29 -0
- package/src/detectors/vue.js +23 -0
- package/src/logger/index.js +44 -0
- package/src/notifications/email.js +53 -0
- package/src/notifications/index.js +34 -0
- package/src/notifications/slack.js +18 -0
- package/src/notifications/webhook.js +21 -0
- package/src/rollback/engine.js +102 -0
- package/src/storage/index.js +109 -0
- package/src/storage/providers/aws.js +96 -0
- package/src/storage/providers/azure.js +45 -0
- package/src/storage/providers/dropbox.js +49 -0
- package/src/storage/providers/ftp.js +69 -0
- package/src/storage/providers/gcp.js +45 -0
- package/src/storage/providers/gdrive.js +80 -0
- package/src/storage/providers/local.js +61 -0
- package/src/utils/author.js +141 -0
- package/src/utils/checksums.js +53 -0
- package/src/utils/firebase-config-generator.js +35 -0
- package/src/utils/github-actions.js +389 -0
- package/src/utils/init-platform.js +229 -0
- package/src/utils/nginx.js +34 -0
- package/src/utils/platform-env.js +132 -0
- package/src/utils/version.js +31 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { createLogger } from '../logger/index.js';
|
|
5
|
+
|
|
6
|
+
function create(config, cwd) {
|
|
7
|
+
const log = createLogger('php');
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
detect() {
|
|
11
|
+
return fs.existsSync(path.join(cwd, 'composer.json'));
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
async install() {
|
|
15
|
+
log.info('Running composer install...');
|
|
16
|
+
await execa('composer', ['install', '--no-dev'], {
|
|
17
|
+
cwd,
|
|
18
|
+
stdio: 'inherit',
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
async test() {
|
|
23
|
+
log.warn('PHP tests not configured, skipping');
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async build() {
|
|
27
|
+
log.info(`Running: ${config.buildCommand}`);
|
|
28
|
+
const [cmd, ...args] = config.buildCommand.split(' ');
|
|
29
|
+
await execa(cmd, args, { cwd, stdio: 'inherit', shell: true });
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async docker() {
|
|
33
|
+
if (await fs.pathExists(path.join(cwd, 'Dockerfile'))) {
|
|
34
|
+
await execa('docker', ['build', '-t', `${config.project}:latest`, '.'], {
|
|
35
|
+
cwd,
|
|
36
|
+
stdio: 'inherit',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default { create };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { createLogger } from '../logger/index.js';
|
|
5
|
+
|
|
6
|
+
function create(config, cwd) {
|
|
7
|
+
const log = createLogger('python');
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
detect() {
|
|
11
|
+
return (
|
|
12
|
+
fs.existsSync(path.join(cwd, 'requirements.txt')) ||
|
|
13
|
+
fs.existsSync(path.join(cwd, 'pyproject.toml'))
|
|
14
|
+
);
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async install() {
|
|
18
|
+
log.info('Installing Python dependencies...');
|
|
19
|
+
if (fs.existsSync(path.join(cwd, 'requirements.txt'))) {
|
|
20
|
+
await execa('pip', ['install', '-r', 'requirements.txt'], {
|
|
21
|
+
cwd,
|
|
22
|
+
stdio: 'inherit',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async test() {
|
|
28
|
+
if (fs.existsSync(path.join(cwd, 'pytest.ini'))) {
|
|
29
|
+
await execa('pytest', [], { cwd, stdio: 'inherit' });
|
|
30
|
+
} else {
|
|
31
|
+
log.warn('No pytest config found, skipping tests');
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async build() {
|
|
36
|
+
log.info(`Running: ${config.buildCommand}`);
|
|
37
|
+
const [cmd, ...args] = config.buildCommand.split(' ');
|
|
38
|
+
await execa(cmd, args, { cwd, stdio: 'inherit', shell: true });
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async docker() {
|
|
42
|
+
if (!(await fs.pathExists(path.join(cwd, 'Dockerfile')))) {
|
|
43
|
+
log.warn('No Dockerfile found, skipping');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await execa('docker', ['build', '-t', `${config.project}:latest`, '.'], {
|
|
47
|
+
cwd,
|
|
48
|
+
stdio: 'inherit',
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default { create };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { createLogger } from '../logger/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
8
|
+
* @param {string} cwd
|
|
9
|
+
*/
|
|
10
|
+
function create(config, cwd) {
|
|
11
|
+
const log = createLogger('rails');
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
detect() {
|
|
15
|
+
return (
|
|
16
|
+
fs.existsSync(path.join(cwd, 'Gemfile')) &&
|
|
17
|
+
(fs.existsSync(path.join(cwd, 'config.ru')) ||
|
|
18
|
+
fs.existsSync(path.join(cwd, 'config', 'application.rb')))
|
|
19
|
+
);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
async install() {
|
|
23
|
+
log.info('Installing Ruby dependencies...');
|
|
24
|
+
await execa('bundle', ['install'], { cwd, stdio: 'inherit' });
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async test() {
|
|
28
|
+
if (await fs.pathExists(path.join(cwd, 'spec'))) {
|
|
29
|
+
log.info('Running RSpec tests...');
|
|
30
|
+
await execa('bundle', ['exec', 'rspec'], { cwd, stdio: 'inherit' });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (await fs.pathExists(path.join(cwd, 'test'))) {
|
|
34
|
+
log.info('Running Rails tests...');
|
|
35
|
+
await execa('bundle', ['exec', 'rails', 'test'], { cwd, stdio: 'inherit' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
log.warn('No spec/ or test/ directory found, skipping tests');
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async build() {
|
|
42
|
+
const buildCommand = config.buildCommand || 'bundle exec rails assets:precompile';
|
|
43
|
+
if (!buildCommand) {
|
|
44
|
+
log.info('No build command configured, skipping');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
log.info(`Running build: ${buildCommand}`);
|
|
48
|
+
const [cmd, ...args] = buildCommand.split(' ');
|
|
49
|
+
await execa(cmd, args, { cwd, stdio: 'inherit', shell: true });
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async docker() {
|
|
53
|
+
if (!(await fs.pathExists(path.join(cwd, 'Dockerfile')))) {
|
|
54
|
+
log.warn('No Dockerfile found, skipping docker build');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
log.info('Building Docker image...');
|
|
58
|
+
await execa('docker', ['build', '-t', `${config.project}:latest`, '.'], {
|
|
59
|
+
cwd,
|
|
60
|
+
stdio: 'inherit',
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default { create };
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import archiver from 'archiver';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import { createLogger } from '../logger/index.js';
|
|
6
|
+
import { generateChecksums, formatChecksums } from '../utils/checksums.js';
|
|
7
|
+
import { getProjectVersion } from '../utils/version.js';
|
|
8
|
+
import { generateNginxConfig } from '../utils/nginx.js';
|
|
9
|
+
import { getGeneratedByMetadata, getArtifactReadmeFooter } from '../utils/author.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} cwd
|
|
13
|
+
* @returns {Promise<{ commit: string, branch: string }>}
|
|
14
|
+
*/
|
|
15
|
+
async function getGitInfo(cwd) {
|
|
16
|
+
try {
|
|
17
|
+
const { stdout: commit } = await execa('git', ['rev-parse', '--short', 'HEAD'], {
|
|
18
|
+
cwd,
|
|
19
|
+
});
|
|
20
|
+
const { stdout: branch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
21
|
+
cwd,
|
|
22
|
+
});
|
|
23
|
+
return { commit: commit.trim(), branch: branch.trim() };
|
|
24
|
+
} catch {
|
|
25
|
+
return { commit: 'unknown', branch: 'unknown' };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} cwd
|
|
31
|
+
* @param {number} [count]
|
|
32
|
+
* @returns {Promise<string>}
|
|
33
|
+
*/
|
|
34
|
+
async function getReleaseNotes(cwd, count = 10) {
|
|
35
|
+
try {
|
|
36
|
+
const { stdout } = await execa(
|
|
37
|
+
'git',
|
|
38
|
+
['log', `-${count}`, '--pretty=format:- %s (%h)'],
|
|
39
|
+
{ cwd }
|
|
40
|
+
);
|
|
41
|
+
return `# Release Notes\n\n${stdout}\n`;
|
|
42
|
+
} catch {
|
|
43
|
+
return '# Release Notes\n\nNo git history available.\n';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
49
|
+
* @returns {'frontend'|'backend'}
|
|
50
|
+
*/
|
|
51
|
+
function resolveArtifactType(config) {
|
|
52
|
+
if (config.projectType === 'both') return 'backend';
|
|
53
|
+
return config.projectType === 'backend' ? 'backend' : 'frontend';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
58
|
+
* @returns {{ buildOutput: string, framework: string, startCommand: string|null, port: number, buildCommand: string|null }}
|
|
59
|
+
*/
|
|
60
|
+
function resolveBuildSettings(config) {
|
|
61
|
+
if (config.projectType === 'both' && config.backend) {
|
|
62
|
+
return {
|
|
63
|
+
buildOutput: config.backend.buildOutput || '.',
|
|
64
|
+
framework: config.backend.framework,
|
|
65
|
+
startCommand: config.backend.startCommand || null,
|
|
66
|
+
port: config.backend.port || 3000,
|
|
67
|
+
buildCommand: config.backend.buildCommand ?? null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
buildOutput: config.buildOutput || 'dist',
|
|
73
|
+
framework: config.framework || 'node',
|
|
74
|
+
startCommand: config.startCommand || null,
|
|
75
|
+
port: config.port || 3000,
|
|
76
|
+
buildCommand: config.buildCommand ?? null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
82
|
+
* @returns {{ buildOutput: string, framework: string }}
|
|
83
|
+
*/
|
|
84
|
+
function resolveFrontendSettings(config) {
|
|
85
|
+
if (config.projectType === 'both' && config.frontend) {
|
|
86
|
+
return {
|
|
87
|
+
buildOutput: config.frontend.buildOutput || 'dist',
|
|
88
|
+
framework: config.frontend.framework,
|
|
89
|
+
buildCommand: config.frontend.buildCommand ?? null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
buildOutput: config.buildOutput || 'dist',
|
|
95
|
+
framework: config.framework || 'react',
|
|
96
|
+
buildCommand: config.buildCommand ?? null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} srcDir
|
|
102
|
+
* @param {string} destDir
|
|
103
|
+
* @param {string} label
|
|
104
|
+
*/
|
|
105
|
+
async function copyIfExists(srcDir, destDir, label) {
|
|
106
|
+
const src = path.join(srcDir, label);
|
|
107
|
+
if (await fs.pathExists(src)) {
|
|
108
|
+
await fs.copy(src, path.join(destDir, label));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {string} cwd
|
|
114
|
+
* @param {string} stagingDir
|
|
115
|
+
* @param {string} dirName
|
|
116
|
+
*/
|
|
117
|
+
async function copyDirectoryIfExists(cwd, stagingDir, dirName) {
|
|
118
|
+
const src = path.join(cwd, dirName);
|
|
119
|
+
if (await fs.pathExists(src)) {
|
|
120
|
+
await fs.copy(src, path.join(stagingDir, dirName));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {string} cwd
|
|
126
|
+
* @param {string} stagingDir
|
|
127
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
128
|
+
*/
|
|
129
|
+
async function stageFrontendArtifact(cwd, stagingDir, config) {
|
|
130
|
+
const frontend = resolveFrontendSettings(config);
|
|
131
|
+
const buildSrc = path.join(cwd, frontend.buildOutput);
|
|
132
|
+
const outputName = path.basename(frontend.buildOutput) || 'dist';
|
|
133
|
+
|
|
134
|
+
if (frontend.buildOutput === '.') {
|
|
135
|
+
if (await fs.pathExists(path.join(cwd, 'index.html'))) {
|
|
136
|
+
await fs.copy(path.join(cwd, 'index.html'), path.join(stagingDir, 'index.html'));
|
|
137
|
+
}
|
|
138
|
+
} else if (await fs.pathExists(buildSrc)) {
|
|
139
|
+
await fs.copy(buildSrc, path.join(stagingDir, outputName));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const file of ['Dockerfile', 'docker-compose.yml', 'package.json', 'nginx.conf']) {
|
|
143
|
+
await copyIfExists(cwd, stagingDir, file);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const hasSshDeploy = (config.deploy || []).some(
|
|
147
|
+
(envName) => config.environments[envName]?.type === 'ssh'
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (hasSshDeploy && !fs.existsSync(path.join(stagingDir, 'nginx.conf'))) {
|
|
151
|
+
const envName = config.deploy?.[0];
|
|
152
|
+
const env = envName ? config.environments[envName] : null;
|
|
153
|
+
const deployPath =
|
|
154
|
+
env?.frontendDeployPath || env?.deployPath || env?.path || `/var/www/${config.project}`;
|
|
155
|
+
const nginxConf = generateNginxConfig(config.project, deployPath, outputName);
|
|
156
|
+
await fs.writeFile(path.join(stagingDir, 'nginx.conf'), nginxConf);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} cwd
|
|
162
|
+
* @param {string} stagingDir
|
|
163
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
164
|
+
*/
|
|
165
|
+
async function stageBackendArtifact(cwd, stagingDir, config) {
|
|
166
|
+
const settings = resolveBuildSettings(config);
|
|
167
|
+
const framework = settings.framework;
|
|
168
|
+
|
|
169
|
+
await copyDirectoryIfExists(cwd, stagingDir, 'src');
|
|
170
|
+
|
|
171
|
+
for (const file of ['Dockerfile', 'docker-compose.yml', '.env.example']) {
|
|
172
|
+
await copyIfExists(cwd, stagingDir, file);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await copyDirectoryIfExists(cwd, stagingDir, 'config');
|
|
176
|
+
await copyDirectoryIfExists(cwd, stagingDir, 'migrations');
|
|
177
|
+
|
|
178
|
+
if (['express', 'nestjs', 'fastify', 'koa', 'nextjs'].includes(framework)) {
|
|
179
|
+
await copyIfExists(cwd, stagingDir, 'package.json');
|
|
180
|
+
} else if (['fastapi', 'django', 'flask'].includes(framework)) {
|
|
181
|
+
await copyIfExists(cwd, stagingDir, 'requirements.txt');
|
|
182
|
+
if (framework === 'django' && (await fs.pathExists(path.join(cwd, 'manage.py')))) {
|
|
183
|
+
await copyIfExists(cwd, stagingDir, 'manage.py');
|
|
184
|
+
}
|
|
185
|
+
} else if (['laravel', 'symfony'].includes(framework)) {
|
|
186
|
+
await copyIfExists(cwd, stagingDir, 'composer.json');
|
|
187
|
+
await copyIfExists(cwd, stagingDir, 'composer.lock');
|
|
188
|
+
} else if (framework === 'spring') {
|
|
189
|
+
await copyIfExists(cwd, stagingDir, 'pom.xml');
|
|
190
|
+
const targetDir = path.join(cwd, 'target');
|
|
191
|
+
if (await fs.pathExists(targetDir)) {
|
|
192
|
+
await fs.ensureDir(path.join(stagingDir, 'target'));
|
|
193
|
+
const jars = (await fs.readdir(targetDir)).filter((f) => f.endsWith('.jar'));
|
|
194
|
+
for (const jar of jars) {
|
|
195
|
+
await fs.copy(path.join(targetDir, jar), path.join(stagingDir, 'target', jar));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else if (framework === 'go') {
|
|
199
|
+
await copyIfExists(cwd, stagingDir, 'go.mod');
|
|
200
|
+
await copyIfExists(cwd, stagingDir, 'go.sum');
|
|
201
|
+
await copyDirectoryIfExists(cwd, stagingDir, 'bin');
|
|
202
|
+
} else if (framework === 'dotnet') {
|
|
203
|
+
const files = await fs.readdir(cwd);
|
|
204
|
+
for (const f of files.filter((name) => name.endsWith('.csproj'))) {
|
|
205
|
+
await copyIfExists(cwd, stagingDir, f);
|
|
206
|
+
}
|
|
207
|
+
await copyDirectoryIfExists(cwd, stagingDir, settings.buildOutput || 'publish');
|
|
208
|
+
} else if (framework === 'rails') {
|
|
209
|
+
await copyIfExists(cwd, stagingDir, 'Gemfile');
|
|
210
|
+
await copyIfExists(cwd, stagingDir, 'Gemfile.lock');
|
|
211
|
+
await copyIfExists(cwd, stagingDir, 'config.ru');
|
|
212
|
+
} else {
|
|
213
|
+
await copyIfExists(cwd, stagingDir, 'package.json');
|
|
214
|
+
await copyIfExists(cwd, stagingDir, 'requirements.txt');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (settings.buildOutput && settings.buildOutput !== '.' && settings.buildOutput !== 'src') {
|
|
218
|
+
const built = path.join(cwd, settings.buildOutput);
|
|
219
|
+
if (await fs.pathExists(built) && !['target', 'bin', 'publish'].includes(settings.buildOutput)) {
|
|
220
|
+
await fs.copy(built, path.join(stagingDir, settings.buildOutput));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
227
|
+
* @param {string} [cwd]
|
|
228
|
+
* @returns {string}
|
|
229
|
+
*/
|
|
230
|
+
export function getArtifactDir(config, cwd = process.cwd()) {
|
|
231
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
232
|
+
return path.join(
|
|
233
|
+
cwd,
|
|
234
|
+
'artifact',
|
|
235
|
+
config.project,
|
|
236
|
+
date,
|
|
237
|
+
`v${config.version}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
243
|
+
* @param {string[]} [deployedTargets]
|
|
244
|
+
* @param {string} [cwd]
|
|
245
|
+
* @returns {Promise<{ artifactDir: string, zipPath: string }>}
|
|
246
|
+
*/
|
|
247
|
+
export async function createArtifact(config, deployedTargets = [], cwd = process.cwd()) {
|
|
248
|
+
const log = createLogger('artifact');
|
|
249
|
+
const version = config.version || (await getProjectVersion(cwd));
|
|
250
|
+
config.version = version;
|
|
251
|
+
|
|
252
|
+
const artifactDir = getArtifactDir(config, cwd);
|
|
253
|
+
const stagingDir = path.join(artifactDir, '_staging');
|
|
254
|
+
await fs.emptyDir(stagingDir);
|
|
255
|
+
|
|
256
|
+
const projectType = config.projectType || 'frontend';
|
|
257
|
+
const artifactType = resolveArtifactType(config);
|
|
258
|
+
const settings = resolveBuildSettings(config);
|
|
259
|
+
|
|
260
|
+
log.info(`Staging ${projectType} artifact...`);
|
|
261
|
+
|
|
262
|
+
if (projectType === 'both') {
|
|
263
|
+
await stageFrontendArtifact(cwd, stagingDir, config);
|
|
264
|
+
const backendStaging = path.join(stagingDir, 'backend');
|
|
265
|
+
await fs.ensureDir(backendStaging);
|
|
266
|
+
await stageBackendArtifact(cwd, backendStaging, config);
|
|
267
|
+
} else if (artifactType === 'backend') {
|
|
268
|
+
await stageBackendArtifact(cwd, stagingDir, config);
|
|
269
|
+
} else {
|
|
270
|
+
await stageFrontendArtifact(cwd, stagingDir, config);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const git = await getGitInfo(cwd);
|
|
274
|
+
const environment = process.env.DEPLOYHUB_ENV || 'production';
|
|
275
|
+
const timestamp = new Date().toISOString();
|
|
276
|
+
|
|
277
|
+
const metadata = {
|
|
278
|
+
project: config.project,
|
|
279
|
+
version,
|
|
280
|
+
timestamp,
|
|
281
|
+
gitCommit: git.commit,
|
|
282
|
+
branch: git.branch,
|
|
283
|
+
environment,
|
|
284
|
+
projectType: artifactType,
|
|
285
|
+
framework: settings.framework,
|
|
286
|
+
buildOutput: settings.buildOutput,
|
|
287
|
+
startCommand: settings.startCommand,
|
|
288
|
+
port: settings.port,
|
|
289
|
+
generatedBy: getGeneratedByMetadata(),
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
if (projectType === 'both') {
|
|
293
|
+
const frontend = resolveFrontendSettings(config);
|
|
294
|
+
metadata.projectType = 'both';
|
|
295
|
+
metadata.frontend = frontend;
|
|
296
|
+
metadata.backend = {
|
|
297
|
+
framework: settings.framework,
|
|
298
|
+
startCommand: settings.startCommand,
|
|
299
|
+
port: settings.port,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const readme = `# ${config.project} Artifact v${version}
|
|
304
|
+
|
|
305
|
+
This artifact was created by DeployHub on ${timestamp}.
|
|
306
|
+
Project type: ${projectType}
|
|
307
|
+
|
|
308
|
+
## Manual Re-deployment
|
|
309
|
+
|
|
310
|
+
1. Extract \`artifact.zip\`
|
|
311
|
+
2. Copy files to your server
|
|
312
|
+
3. Follow framework-specific start instructions in deployment.json
|
|
313
|
+
|
|
314
|
+
No other tooling required — everything needed is in this archive.
|
|
315
|
+
|
|
316
|
+
${getArtifactReadmeFooter()}`;
|
|
317
|
+
|
|
318
|
+
await fs.writeFile(path.join(stagingDir, 'README.md'), readme);
|
|
319
|
+
await fs.writeJson(path.join(stagingDir, 'metadata.json'), metadata, { spaces: 2 });
|
|
320
|
+
await fs.writeJson(path.join(artifactDir, 'metadata.json'), metadata, { spaces: 2 });
|
|
321
|
+
await fs.writeFile(
|
|
322
|
+
path.join(artifactDir, 'release-notes.md'),
|
|
323
|
+
await getReleaseNotes(cwd)
|
|
324
|
+
);
|
|
325
|
+
await fs.writeFile(path.join(artifactDir, 'README.md'), readme);
|
|
326
|
+
await fs.writeJson(
|
|
327
|
+
path.join(artifactDir, 'deployment.json'),
|
|
328
|
+
{ targets: deployedTargets, deployedAt: timestamp },
|
|
329
|
+
{ spaces: 2 }
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const logsContent = `[${timestamp}] Artifact created for ${config.project} v${version} (${projectType})\n`;
|
|
333
|
+
await fs.writeFile(path.join(artifactDir, 'logs.txt'), logsContent);
|
|
334
|
+
await fs.writeFile(path.join(stagingDir, 'logs.txt'), logsContent);
|
|
335
|
+
|
|
336
|
+
log.info('Creating zip archive...');
|
|
337
|
+
const zipPath = path.join(artifactDir, 'artifact.zip');
|
|
338
|
+
await createZip(stagingDir, zipPath);
|
|
339
|
+
|
|
340
|
+
const checksums = await generateChecksums(stagingDir);
|
|
341
|
+
const checksumContent = formatChecksums(checksums);
|
|
342
|
+
await fs.writeFile(path.join(artifactDir, 'checksums.txt'), checksumContent);
|
|
343
|
+
await fs.writeFile(path.join(stagingDir, 'checksums.txt'), checksumContent);
|
|
344
|
+
await fs.writeJson(
|
|
345
|
+
path.join(stagingDir, 'deployment.json'),
|
|
346
|
+
{ targets: deployedTargets, deployedAt: timestamp },
|
|
347
|
+
{ spaces: 2 }
|
|
348
|
+
);
|
|
349
|
+
await fs.writeFile(
|
|
350
|
+
path.join(stagingDir, 'release-notes.md'),
|
|
351
|
+
await getReleaseNotes(cwd)
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
await fs.remove(stagingDir);
|
|
355
|
+
log.success(`Artifact created at ${artifactDir}`);
|
|
356
|
+
|
|
357
|
+
return { artifactDir, zipPath };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @param {string} sourceDir
|
|
362
|
+
* @param {string} zipPath
|
|
363
|
+
*/
|
|
364
|
+
function createZip(sourceDir, zipPath) {
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
const output = fs.createWriteStream(zipPath);
|
|
367
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
368
|
+
|
|
369
|
+
output.on('close', resolve);
|
|
370
|
+
archive.on('error', reject);
|
|
371
|
+
|
|
372
|
+
archive.pipe(output);
|
|
373
|
+
archive.directory(sourceDir, false);
|
|
374
|
+
archive.finalize();
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @param {string} [cwd]
|
|
380
|
+
* @returns {Promise<Array<{ project: string, date: string, version: string, path: string, size: number }>>}
|
|
381
|
+
*/
|
|
382
|
+
export async function listLocalArtifacts(cwd = process.cwd()) {
|
|
383
|
+
const artifactRoot = path.join(cwd, 'artifact');
|
|
384
|
+
if (!(await fs.pathExists(artifactRoot))) return [];
|
|
385
|
+
|
|
386
|
+
/** @type {Array<{ project: string, date: string, version: string, path: string, size: number }>} */
|
|
387
|
+
const results = [];
|
|
388
|
+
|
|
389
|
+
const projects = await fs.readdir(artifactRoot);
|
|
390
|
+
for (const project of projects) {
|
|
391
|
+
const projectDir = path.join(artifactRoot, project);
|
|
392
|
+
if (!(await fs.stat(projectDir)).isDirectory()) continue;
|
|
393
|
+
|
|
394
|
+
const dates = await fs.readdir(projectDir);
|
|
395
|
+
for (const date of dates) {
|
|
396
|
+
const dateDir = path.join(projectDir, date);
|
|
397
|
+
if (!(await fs.stat(dateDir)).isDirectory()) continue;
|
|
398
|
+
|
|
399
|
+
const versions = await fs.readdir(dateDir);
|
|
400
|
+
for (const version of versions) {
|
|
401
|
+
const versionDir = path.join(dateDir, version);
|
|
402
|
+
const zipPath = path.join(versionDir, 'artifact.zip');
|
|
403
|
+
if (await fs.pathExists(zipPath)) {
|
|
404
|
+
const stat = await fs.stat(zipPath);
|
|
405
|
+
results.push({
|
|
406
|
+
project,
|
|
407
|
+
date,
|
|
408
|
+
version: version.replace(/^v/, ''),
|
|
409
|
+
path: versionDir,
|
|
410
|
+
size: stat.size,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return results.sort((a, b) => b.date.localeCompare(a.date));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* @param {string} versionDir
|
|
422
|
+
* @param {string} extractTo
|
|
423
|
+
*/
|
|
424
|
+
export async function extractArtifact(versionDir, extractTo) {
|
|
425
|
+
const zipPath = path.join(versionDir, 'artifact.zip');
|
|
426
|
+
if (!(await fs.pathExists(zipPath))) {
|
|
427
|
+
throw new Error(`Artifact zip not found at ${zipPath}`);
|
|
428
|
+
}
|
|
429
|
+
await fs.ensureDir(extractTo);
|
|
430
|
+
const { execa: execaFn } = await import('execa');
|
|
431
|
+
if (process.platform === 'win32') {
|
|
432
|
+
await execaFn(
|
|
433
|
+
'powershell',
|
|
434
|
+
['-Command', `Expand-Archive -Path "${zipPath}" -DestinationPath "${extractTo}" -Force`],
|
|
435
|
+
{ stdio: 'inherit' }
|
|
436
|
+
);
|
|
437
|
+
} else {
|
|
438
|
+
await execaFn('unzip', ['-o', zipPath, '-d', extractTo], { stdio: 'inherit' });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Rebuild artifact.zip after deploy so deployment.json is included for remote rollback.
|
|
444
|
+
* @param {string} artifactDir
|
|
445
|
+
*/
|
|
446
|
+
export async function repackArtifactZip(artifactDir) {
|
|
447
|
+
const zipPath = path.join(artifactDir, 'artifact.zip');
|
|
448
|
+
if (!(await fs.pathExists(zipPath))) {
|
|
449
|
+
throw new Error(`Artifact zip not found at ${zipPath}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const tempDir = path.join(artifactDir, '_repack');
|
|
453
|
+
await fs.emptyDir(tempDir);
|
|
454
|
+
await extractArtifact(artifactDir, tempDir);
|
|
455
|
+
|
|
456
|
+
const deploymentPath = path.join(artifactDir, 'deployment.json');
|
|
457
|
+
if (await fs.pathExists(deploymentPath)) {
|
|
458
|
+
await fs.copy(deploymentPath, path.join(tempDir, 'deployment.json'));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
for (const file of ['metadata.json', 'logs.txt', 'checksums.txt', 'release-notes.md', 'README.md']) {
|
|
462
|
+
const src = path.join(artifactDir, file);
|
|
463
|
+
if (await fs.pathExists(src)) {
|
|
464
|
+
await fs.copy(src, path.join(tempDir, file));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
await fs.remove(zipPath);
|
|
469
|
+
await createZip(tempDir, zipPath);
|
|
470
|
+
await fs.remove(tempDir);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export default { createArtifact, listLocalArtifacts, extractArtifact, getArtifactDir, repackArtifactZip };
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { loadEnv } from '../core/config.js';
|
|
5
|
+
import { registerInitCommand } from '../commands/init.js';
|
|
6
|
+
import { registerBuildCommand } from '../commands/build.js';
|
|
7
|
+
import { registerArtifactCommand } from '../commands/artifact.js';
|
|
8
|
+
import { registerStorageCommand } from '../commands/storage.js';
|
|
9
|
+
import { registerDeployCommand } from '../commands/deploy.js';
|
|
10
|
+
import { registerRollbackCommand } from '../commands/rollback.js';
|
|
11
|
+
import { registerLogsCommand } from '../commands/logs.js';
|
|
12
|
+
import { registerDoctorCommand } from '../commands/doctor.js';
|
|
13
|
+
import { registerVerifyCommand } from '../commands/verify.js';
|
|
14
|
+
import { registerCleanCommand } from '../commands/clean.js';
|
|
15
|
+
import { registerUpdateCommand } from '../commands/update.js';
|
|
16
|
+
import { formatVersionOutput, printBanner, shouldShowBanner } from '../utils/author.js';
|
|
17
|
+
|
|
18
|
+
loadEnv();
|
|
19
|
+
|
|
20
|
+
const program = new Command();
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.name('deployhub')
|
|
24
|
+
.description('Zero-configuration deployment and artifact manager')
|
|
25
|
+
.version(formatVersionOutput(), '-V, --version', 'output the version number');
|
|
26
|
+
|
|
27
|
+
program.hook('preAction', () => {
|
|
28
|
+
if (shouldShowBanner()) {
|
|
29
|
+
printBanner();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
registerInitCommand(program);
|
|
34
|
+
registerBuildCommand(program);
|
|
35
|
+
registerArtifactCommand(program);
|
|
36
|
+
registerStorageCommand(program);
|
|
37
|
+
registerDeployCommand(program);
|
|
38
|
+
registerRollbackCommand(program);
|
|
39
|
+
registerLogsCommand(program);
|
|
40
|
+
registerDoctorCommand(program);
|
|
41
|
+
registerVerifyCommand(program);
|
|
42
|
+
registerCleanCommand(program);
|
|
43
|
+
registerUpdateCommand(program);
|
|
44
|
+
|
|
45
|
+
program.parse();
|