@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,210 @@
|
|
|
1
|
+
import { detectFramework } from '../detectors/index.js';
|
|
2
|
+
import { getAdapter } from '../adapters/index.js';
|
|
3
|
+
import { createArtifact, repackArtifactZip } from '../artifact/engine.js';
|
|
4
|
+
import { uploadToAll } from '../storage/index.js';
|
|
5
|
+
import { deployToAll } from '../deployment/index.js';
|
|
6
|
+
import { sendNotifications } from '../notifications/index.js';
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { getProjectVersion } from '../utils/version.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
14
|
+
* @param {string} cwd
|
|
15
|
+
* @param {Record<string, unknown>} state
|
|
16
|
+
*/
|
|
17
|
+
export function buildPipelineStages(config, cwd, state) {
|
|
18
|
+
/** @type {import('../core/pipeline.js').PipelineStage[]} */
|
|
19
|
+
const stages = [
|
|
20
|
+
{
|
|
21
|
+
name: 'detect',
|
|
22
|
+
async run(ctx) {
|
|
23
|
+
const detected = await detectFramework(ctx.cwd);
|
|
24
|
+
if (detected) {
|
|
25
|
+
if (!ctx.config.framework) ctx.config.framework = detected.framework;
|
|
26
|
+
if (!ctx.config.projectType && detected.projectType) {
|
|
27
|
+
ctx.config.projectType = detected.projectType;
|
|
28
|
+
}
|
|
29
|
+
if (!ctx.config.language && detected.language) {
|
|
30
|
+
ctx.config.language = detected.language;
|
|
31
|
+
}
|
|
32
|
+
if (ctx.config.buildCommand === undefined && detected.buildCommand !== undefined) {
|
|
33
|
+
ctx.config.buildCommand = detected.buildCommand;
|
|
34
|
+
}
|
|
35
|
+
if (!ctx.config.buildOutput && detected.buildOutput) {
|
|
36
|
+
ctx.config.buildOutput = detected.buildOutput;
|
|
37
|
+
}
|
|
38
|
+
if (!ctx.config.startCommand && detected.startCommand) {
|
|
39
|
+
ctx.config.startCommand = detected.startCommand;
|
|
40
|
+
}
|
|
41
|
+
if (!ctx.config.port && detected.port) {
|
|
42
|
+
ctx.config.port = detected.port;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
ctx.state.framework = ctx.config.framework;
|
|
46
|
+
ctx.state.projectType = ctx.config.projectType || 'frontend';
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'install',
|
|
51
|
+
async run(ctx) {
|
|
52
|
+
const adapter = getAdapter(ctx.config.framework, ctx.config, ctx.cwd);
|
|
53
|
+
await adapter.install();
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'test',
|
|
58
|
+
enabled: (ctx) => ctx.config.pipeline.test === true,
|
|
59
|
+
async run(ctx) {
|
|
60
|
+
const adapter = getAdapter(ctx.config.framework, ctx.config, ctx.cwd);
|
|
61
|
+
await adapter.test();
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'build',
|
|
66
|
+
async run(ctx) {
|
|
67
|
+
if (ctx.config.projectType === 'both') {
|
|
68
|
+
if (ctx.config.frontend?.buildCommand) {
|
|
69
|
+
const frontendAdapter = getAdapter(
|
|
70
|
+
ctx.config.frontend.framework,
|
|
71
|
+
ctx.config,
|
|
72
|
+
ctx.cwd
|
|
73
|
+
);
|
|
74
|
+
const saved = ctx.config.buildCommand;
|
|
75
|
+
ctx.config.buildCommand = ctx.config.frontend.buildCommand;
|
|
76
|
+
await frontendAdapter.build();
|
|
77
|
+
ctx.config.buildCommand = saved;
|
|
78
|
+
}
|
|
79
|
+
if (ctx.config.backend?.buildCommand) {
|
|
80
|
+
const backendAdapter = getAdapter(
|
|
81
|
+
ctx.config.backend.framework,
|
|
82
|
+
ctx.config,
|
|
83
|
+
ctx.cwd
|
|
84
|
+
);
|
|
85
|
+
const saved = ctx.config.buildCommand;
|
|
86
|
+
ctx.config.buildCommand = ctx.config.backend.buildCommand;
|
|
87
|
+
await backendAdapter.build();
|
|
88
|
+
ctx.config.buildCommand = saved;
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const adapter = getAdapter(ctx.config.framework, ctx.config, ctx.cwd);
|
|
94
|
+
await adapter.build();
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'docker',
|
|
99
|
+
enabled: (ctx) =>
|
|
100
|
+
ctx.config.pipeline.docker === true && ctx.config.docker === true,
|
|
101
|
+
async run(ctx) {
|
|
102
|
+
const adapter = getAdapter(ctx.config.framework, ctx.config, ctx.cwd);
|
|
103
|
+
await adapter.docker();
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'artifact',
|
|
108
|
+
enabled: (ctx) => ctx.config.artifact !== false,
|
|
109
|
+
async run(ctx) {
|
|
110
|
+
ctx.config.version = await getProjectVersion(ctx.cwd);
|
|
111
|
+
const result = await createArtifact(
|
|
112
|
+
ctx.config,
|
|
113
|
+
/** @type {string[]} */ (ctx.state.deployedTargets || []),
|
|
114
|
+
ctx.cwd
|
|
115
|
+
);
|
|
116
|
+
ctx.state.artifactDir = result.artifactDir;
|
|
117
|
+
ctx.state.zipPath = result.zipPath;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'storage',
|
|
122
|
+
enabled: (ctx) => {
|
|
123
|
+
const willDeploy =
|
|
124
|
+
ctx.config.pipeline.deploy === true && (ctx.config.deploy?.length || 0) > 0;
|
|
125
|
+
if (willDeploy && (!ctx.config.storage || ctx.config.storage.length === 0)) {
|
|
126
|
+
ctx.config.storage = ['local'];
|
|
127
|
+
}
|
|
128
|
+
return (ctx.config.storage?.length || 0) > 0;
|
|
129
|
+
},
|
|
130
|
+
async run(ctx) {
|
|
131
|
+
const zipPath = /** @type {string} */ (ctx.state.zipPath);
|
|
132
|
+
if (!zipPath) throw new Error('No artifact to upload');
|
|
133
|
+
await uploadToAll(ctx.config.storage, zipPath, ctx.config);
|
|
134
|
+
ctx.state.storageCompleted = true;
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'deploy',
|
|
139
|
+
enabled: (ctx) =>
|
|
140
|
+
ctx.config.pipeline.deploy === true && (ctx.config.deploy?.length || 0) > 0,
|
|
141
|
+
async run(ctx) {
|
|
142
|
+
if (!ctx.state.storageCompleted) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
'Storage upload must complete before deploy. Enable at least one storage provider.'
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const artifactDir = /** @type {string} */ (ctx.state.artifactDir);
|
|
148
|
+
if (!artifactDir) throw new Error('No artifact to deploy');
|
|
149
|
+
const deployed = await deployToAll(ctx.config, artifactDir);
|
|
150
|
+
ctx.state.deployedTargets = deployed;
|
|
151
|
+
|
|
152
|
+
const deploymentPath = path.join(artifactDir, 'deployment.json');
|
|
153
|
+
if (await fs.pathExists(deploymentPath)) {
|
|
154
|
+
const data = await fs.readJson(deploymentPath);
|
|
155
|
+
const last = data.lastDeployment;
|
|
156
|
+
if (last?.deployUrl || last?.deploymentUrl) {
|
|
157
|
+
ctx.state.lastDeployUrl = last.deployUrl || last.deploymentUrl;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await repackArtifactZip(artifactDir);
|
|
162
|
+
const zipPath = /** @type {string} */ (ctx.state.zipPath);
|
|
163
|
+
if (zipPath) {
|
|
164
|
+
await uploadToAll(ctx.config.storage, zipPath, ctx.config);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'verify',
|
|
170
|
+
enabled: (ctx) =>
|
|
171
|
+
ctx.config.pipeline.verify === true && !!ctx.config.healthCheck?.url,
|
|
172
|
+
async run(ctx) {
|
|
173
|
+
const url = ctx.config.healthCheck.url;
|
|
174
|
+
const timeout = (ctx.config.healthCheck.timeout || 30) * 1000;
|
|
175
|
+
const start = Date.now();
|
|
176
|
+
const response = await axios.get(url, {
|
|
177
|
+
timeout,
|
|
178
|
+
validateStatus: () => true,
|
|
179
|
+
});
|
|
180
|
+
const elapsed = Date.now() - start;
|
|
181
|
+
if (response.status < 200 || response.status >= 400) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Health check failed: HTTP ${response.status} (${elapsed}ms)`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
ctx.state.healthCheck = { status: response.status, elapsed };
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'notify',
|
|
191
|
+
enabled: (ctx) => ctx.config.pipeline.notify === true,
|
|
192
|
+
async run(ctx) {
|
|
193
|
+
const lastDeploy = ctx.state.lastDeployUrl;
|
|
194
|
+
await sendNotifications(ctx.config, {
|
|
195
|
+
success: !ctx.state.failure,
|
|
196
|
+
version: ctx.config.version,
|
|
197
|
+
message: ctx.state.failure
|
|
198
|
+
? String(ctx.state.failure)
|
|
199
|
+
: 'Build and deploy completed',
|
|
200
|
+
deployUrl: typeof lastDeploy === 'string' ? lastDeploy : ctx.config.healthCheck?.url,
|
|
201
|
+
environment: process.env.DEPLOYHUB_ENV || 'production',
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
return stages;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export default { buildPipelineStages };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createSshProvider } from './providers/ssh.js';
|
|
2
|
+
import { createDockerProvider } from './providers/docker.js';
|
|
3
|
+
import { createEc2Provider } from './providers/ec2.js';
|
|
4
|
+
import { createAzureVmProvider } from './providers/azure-vm.js';
|
|
5
|
+
import { createGcpVmProvider } from './providers/gcp-vm.js';
|
|
6
|
+
import { createKubernetesProvider } from './providers/kubernetes.js';
|
|
7
|
+
import { createPlatformProvider } from './providers/platforms/index.js';
|
|
8
|
+
import { createLogger } from '../logger/index.js';
|
|
9
|
+
|
|
10
|
+
/** @type {Record<string, Function>} */
|
|
11
|
+
const PROVIDER_FACTORIES = {
|
|
12
|
+
ssh: createSshProvider,
|
|
13
|
+
docker: createDockerProvider,
|
|
14
|
+
ec2: createEc2Provider,
|
|
15
|
+
'azure-vm': createAzureVmProvider,
|
|
16
|
+
'gcp-vm': createGcpVmProvider,
|
|
17
|
+
kubernetes: createKubernetesProvider,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Record<string, unknown>} environment
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
function isPlatformDeployment(environment) {
|
|
25
|
+
return environment.deploymentType === 'platform';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {Record<string, unknown>} environment
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
function isHybridEnvironment(environment) {
|
|
33
|
+
return (
|
|
34
|
+
environment.frontendDeploymentType === 'platform' &&
|
|
35
|
+
(environment.backendDeploymentType === 'server' || !!environment.type)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} type
|
|
41
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
42
|
+
* @param {string} envName
|
|
43
|
+
* @param {Record<string, string>} [env]
|
|
44
|
+
*/
|
|
45
|
+
export function getDeploymentProvider(type, config, envName, env = process.env) {
|
|
46
|
+
const environment = config.environments[envName];
|
|
47
|
+
if (!environment) {
|
|
48
|
+
throw new Error(`Environment "${envName}" not found in config`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isPlatformDeployment(environment)) {
|
|
52
|
+
return createPlatformProvider(environment.platform, config, envName, env);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const providerType = type || environment.type;
|
|
56
|
+
const factory = PROVIDER_FACTORIES[providerType];
|
|
57
|
+
if (!factory) {
|
|
58
|
+
throw new Error(`Unknown deployment provider: ${providerType}`);
|
|
59
|
+
}
|
|
60
|
+
return factory(config, envName, env);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
65
|
+
* @param {string} artifactDir
|
|
66
|
+
* @param {string[]} [envNames]
|
|
67
|
+
*/
|
|
68
|
+
export async function deployToAll(config, artifactDir, envNames) {
|
|
69
|
+
const log = createLogger('deploy');
|
|
70
|
+
const targets = envNames || config.deploy || [];
|
|
71
|
+
|
|
72
|
+
if (targets.length === 0) {
|
|
73
|
+
log.warn('No deployment targets configured, skipping');
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const deployed = [];
|
|
78
|
+
for (const envName of targets) {
|
|
79
|
+
const envConfig = config.environments[envName];
|
|
80
|
+
if (!envConfig) {
|
|
81
|
+
throw new Error(`Environment "${envName}" not found in config`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isHybridEnvironment(envConfig)) {
|
|
85
|
+
log.info(`Deploying hybrid target ${envName} (platform frontend + server backend)...`);
|
|
86
|
+
|
|
87
|
+
const platformProvider = createPlatformProvider(
|
|
88
|
+
envConfig.platform,
|
|
89
|
+
config,
|
|
90
|
+
envName
|
|
91
|
+
);
|
|
92
|
+
await platformProvider.deploy(artifactDir);
|
|
93
|
+
log.success(`Frontend deployed to ${envConfig.platform}`);
|
|
94
|
+
|
|
95
|
+
const serverProvider = getDeploymentProvider(envConfig.type, config, envName);
|
|
96
|
+
await serverProvider.deploy(artifactDir);
|
|
97
|
+
log.success(`Backend deployed via ${envConfig.type}`);
|
|
98
|
+
|
|
99
|
+
deployed.push(envName);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isPlatformDeployment(envConfig)) {
|
|
104
|
+
const provider = createPlatformProvider(envConfig.platform, config, envName);
|
|
105
|
+
log.info(`Deploying to ${envName} (${envConfig.platform})...`);
|
|
106
|
+
await provider.deploy(artifactDir);
|
|
107
|
+
deployed.push(envName);
|
|
108
|
+
log.success(`Deployed to ${envName}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const provider = getDeploymentProvider(envConfig.type, config, envName);
|
|
113
|
+
log.info(`Deploying to ${envName} (${envConfig.type})...`);
|
|
114
|
+
await provider.deploy(artifactDir);
|
|
115
|
+
deployed.push(envName);
|
|
116
|
+
log.success(`Deployed to ${envName}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return deployed;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
124
|
+
* @param {string} artifactDir
|
|
125
|
+
* @param {string[]} [envNames]
|
|
126
|
+
*/
|
|
127
|
+
export async function rollbackAll(config, artifactDir, envNames) {
|
|
128
|
+
const targets = envNames || config.deploy || [];
|
|
129
|
+
for (const envName of targets) {
|
|
130
|
+
const envConfig = config.environments[envName];
|
|
131
|
+
|
|
132
|
+
if (isHybridEnvironment(envConfig)) {
|
|
133
|
+
const platformProvider = createPlatformProvider(
|
|
134
|
+
envConfig.platform,
|
|
135
|
+
config,
|
|
136
|
+
envName
|
|
137
|
+
);
|
|
138
|
+
await platformProvider.rollback(artifactDir);
|
|
139
|
+
|
|
140
|
+
const serverProvider = getDeploymentProvider(envConfig.type, config, envName);
|
|
141
|
+
await serverProvider.rollback(artifactDir);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (isPlatformDeployment(envConfig)) {
|
|
146
|
+
const provider = createPlatformProvider(envConfig.platform, config, envName);
|
|
147
|
+
await provider.rollback(artifactDir);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const provider = getDeploymentProvider(envConfig.type, config, envName);
|
|
152
|
+
await provider.rollback(artifactDir);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default { getDeploymentProvider, deployToAll, rollbackAll };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { createLogger } from '../../logger/index.js';
|
|
3
|
+
|
|
4
|
+
export function createDockerProvider(config, envName) {
|
|
5
|
+
const log = createLogger('docker');
|
|
6
|
+
|
|
7
|
+
async function deploy(artifactDir) {
|
|
8
|
+
log.info('Deploying via Docker...');
|
|
9
|
+
await execa('docker', ['compose', 'up', '-d', '--build'], {
|
|
10
|
+
cwd: artifactDir,
|
|
11
|
+
stdio: 'inherit',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function rollback(artifactDir) {
|
|
16
|
+
await deploy(artifactDir);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function healthCheck() {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function testConnection() {
|
|
24
|
+
await execa('docker', ['info'], { stdio: 'pipe' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { deploy, rollback, healthCheck, testConnection };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default { createDockerProvider };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { createLogger } from '../../logger/index.js';
|
|
3
|
+
|
|
4
|
+
export function createKubernetesProvider(config, envName) {
|
|
5
|
+
const log = createLogger('kubernetes');
|
|
6
|
+
|
|
7
|
+
async function deploy(artifactDir) {
|
|
8
|
+
log.info('Deploying to Kubernetes...');
|
|
9
|
+
await execa('kubectl', ['apply', '-f', '.'], {
|
|
10
|
+
cwd: artifactDir,
|
|
11
|
+
stdio: 'inherit',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function rollback(artifactDir) {
|
|
16
|
+
await deploy(artifactDir);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function healthCheck() {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function testConnection() {
|
|
24
|
+
await execa('kubectl', ['cluster-info'], { stdio: 'pipe' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { deploy, rollback, healthCheck, testConnection };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default { createKubernetesProvider };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {import('../../../core/config.js').DeployHubConfig} config
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function getBuildOutputDir(config) {
|
|
10
|
+
if (config.projectType === 'both' && config.frontend?.buildOutput) {
|
|
11
|
+
return config.frontend.buildOutput;
|
|
12
|
+
}
|
|
13
|
+
return config.buildOutput || 'dist';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {import('../../../core/config.js').DeployHubConfig} config
|
|
18
|
+
* @param {string} [cwd]
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
export function resolveBuildOutputPath(config, cwd = process.cwd()) {
|
|
22
|
+
return path.join(cwd, getBuildOutputDir(config));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} artifactDir
|
|
27
|
+
* @param {Record<string, unknown>} record
|
|
28
|
+
* @param {string} [environmentName]
|
|
29
|
+
*/
|
|
30
|
+
export async function saveDeploymentRecord(artifactDir, record, environmentName = '') {
|
|
31
|
+
const deploymentPath = path.join(artifactDir, 'deployment.json');
|
|
32
|
+
let existing = { targets: [], deployedAt: new Date().toISOString(), deployments: [] };
|
|
33
|
+
|
|
34
|
+
if (await fs.pathExists(deploymentPath)) {
|
|
35
|
+
existing = await fs.readJson(deploymentPath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const timestamp = new Date().toISOString();
|
|
39
|
+
const envName = record.environmentName || environmentName || record.envName || '';
|
|
40
|
+
|
|
41
|
+
/** @type {Record<string, unknown>} */
|
|
42
|
+
const entry = {
|
|
43
|
+
...record,
|
|
44
|
+
platform: record.platform,
|
|
45
|
+
deployId: record.deployId || record.deploymentId || '',
|
|
46
|
+
deployUrl: record.deployUrl || record.deploymentUrl || '',
|
|
47
|
+
deploymentId: record.deploymentId || record.deployId || '',
|
|
48
|
+
deploymentUrl: record.deploymentUrl || record.deployUrl || '',
|
|
49
|
+
timestamp,
|
|
50
|
+
environmentName: envName,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const deployments = existing.deployments || existing.platformDeployments || [];
|
|
54
|
+
deployments.push(entry);
|
|
55
|
+
|
|
56
|
+
await fs.writeJson(
|
|
57
|
+
deploymentPath,
|
|
58
|
+
{
|
|
59
|
+
...existing,
|
|
60
|
+
deployments,
|
|
61
|
+
platformDeployments: deployments,
|
|
62
|
+
lastDeployment: entry,
|
|
63
|
+
deployedAt: timestamp,
|
|
64
|
+
},
|
|
65
|
+
{ spaces: 2 }
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} artifactDir
|
|
71
|
+
* @returns {Promise<Record<string, unknown>|null>}
|
|
72
|
+
*/
|
|
73
|
+
export async function readLastPlatformDeployment(artifactDir) {
|
|
74
|
+
const deploymentPath = path.join(artifactDir, 'deployment.json');
|
|
75
|
+
if (!(await fs.pathExists(deploymentPath))) return null;
|
|
76
|
+
const data = await fs.readJson(deploymentPath);
|
|
77
|
+
const deployments = data.deployments || data.platformDeployments || [];
|
|
78
|
+
return data.lastDeployment || deployments.at(-1) || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} artifactDir
|
|
83
|
+
* @returns {Promise<Record<string, unknown>|null>}
|
|
84
|
+
*/
|
|
85
|
+
export async function readPreviousPlatformDeployment(artifactDir) {
|
|
86
|
+
const deploymentPath = path.join(artifactDir, 'deployment.json');
|
|
87
|
+
if (!(await fs.pathExists(deploymentPath))) return null;
|
|
88
|
+
const data = await fs.readJson(deploymentPath);
|
|
89
|
+
const deployments = data.deployments || data.platformDeployments || [];
|
|
90
|
+
if (deployments.length < 2) return null;
|
|
91
|
+
return deployments[deployments.length - 2];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} artifactDir
|
|
96
|
+
* @param {string} [environmentName]
|
|
97
|
+
* @returns {Promise<Record<string, unknown>|null>}
|
|
98
|
+
*/
|
|
99
|
+
export async function readDeploymentForEnvironment(artifactDir, environmentName) {
|
|
100
|
+
const deploymentPath = path.join(artifactDir, 'deployment.json');
|
|
101
|
+
if (!(await fs.pathExists(deploymentPath))) return null;
|
|
102
|
+
const data = await fs.readJson(deploymentPath);
|
|
103
|
+
const deployments = data.deployments || data.platformDeployments || [];
|
|
104
|
+
if (environmentName) {
|
|
105
|
+
const match = deployments.filter((d) => d.environmentName === environmentName);
|
|
106
|
+
return match.at(-1) || null;
|
|
107
|
+
}
|
|
108
|
+
return data.lastDeployment || deployments.at(-1) || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} command
|
|
113
|
+
* @param {string} [cwd]
|
|
114
|
+
* @param {Record<string, string>} [env]
|
|
115
|
+
*/
|
|
116
|
+
export async function runCli(command, cwd = process.cwd(), env = process.env) {
|
|
117
|
+
const [cmd, ...args] = command.split(' ');
|
|
118
|
+
const result = await execa(cmd, args, {
|
|
119
|
+
cwd,
|
|
120
|
+
env: { ...process.env, ...env },
|
|
121
|
+
shell: true,
|
|
122
|
+
reject: false,
|
|
123
|
+
});
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {string} binary
|
|
129
|
+
* @returns {Promise<boolean>}
|
|
130
|
+
*/
|
|
131
|
+
export async function isCliInstalled(binary) {
|
|
132
|
+
try {
|
|
133
|
+
const result = await execa(binary, ['--version'], { reject: false });
|
|
134
|
+
return result.exitCode === 0;
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} url
|
|
142
|
+
* @returns {Promise<boolean>}
|
|
143
|
+
*/
|
|
144
|
+
export async function checkUrlHealth(url) {
|
|
145
|
+
try {
|
|
146
|
+
const axios = (await import('axios')).default;
|
|
147
|
+
const response = await axios.get(url, {
|
|
148
|
+
timeout: 30000,
|
|
149
|
+
validateStatus: () => true,
|
|
150
|
+
});
|
|
151
|
+
return response.status >= 200 && response.status < 400;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default {
|
|
158
|
+
getBuildOutputDir,
|
|
159
|
+
resolveBuildOutputPath,
|
|
160
|
+
saveDeploymentRecord,
|
|
161
|
+
readLastPlatformDeployment,
|
|
162
|
+
readPreviousPlatformDeployment,
|
|
163
|
+
readDeploymentForEnvironment,
|
|
164
|
+
runCli,
|
|
165
|
+
isCliInstalled,
|
|
166
|
+
checkUrlHealth,
|
|
167
|
+
};
|