@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,28 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
function detect(cwd = process.cwd()) {
|
|
5
|
+
return fs.existsSync(path.join(cwd, 'package.json'));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getInfo(cwd = process.cwd()) {
|
|
9
|
+
const pkg = fs.readJsonSync(path.join(cwd, 'package.json'));
|
|
10
|
+
const scripts = pkg.scripts || {};
|
|
11
|
+
let buildCommand = 'npm install';
|
|
12
|
+
if (scripts.build) {
|
|
13
|
+
buildCommand = 'npm run build';
|
|
14
|
+
} else if (scripts.start) {
|
|
15
|
+
buildCommand = 'npm install';
|
|
16
|
+
}
|
|
17
|
+
const outputCandidates = ['dist', 'build', 'out', 'public'];
|
|
18
|
+
const buildOutput =
|
|
19
|
+
outputCandidates.find((d) => fs.existsSync(path.join(cwd, d))) || 'dist';
|
|
20
|
+
return {
|
|
21
|
+
framework: 'node',
|
|
22
|
+
buildCommand,
|
|
23
|
+
buildOutput,
|
|
24
|
+
hasDocker: fs.existsSync(path.join(cwd, 'Dockerfile')),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default { detect, getInfo };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
function detect(cwd = process.cwd()) {
|
|
5
|
+
return fs.existsSync(path.join(cwd, 'composer.json'));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getInfo(cwd = process.cwd()) {
|
|
9
|
+
return {
|
|
10
|
+
framework: 'php',
|
|
11
|
+
buildCommand: 'composer install --no-dev --optimize-autoloader',
|
|
12
|
+
buildOutput: 'public',
|
|
13
|
+
hasDocker: fs.existsSync(path.join(cwd, 'Dockerfile')),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default { detect, getInfo };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
function detect(cwd = process.cwd()) {
|
|
5
|
+
return (
|
|
6
|
+
fs.existsSync(path.join(cwd, 'requirements.txt')) ||
|
|
7
|
+
fs.existsSync(path.join(cwd, 'pyproject.toml')) ||
|
|
8
|
+
fs.existsSync(path.join(cwd, 'setup.py'))
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getInfo(cwd = process.cwd()) {
|
|
13
|
+
const hasDocker = fs.existsSync(path.join(cwd, 'Dockerfile'));
|
|
14
|
+
return {
|
|
15
|
+
framework: 'python',
|
|
16
|
+
buildCommand: 'pip install -r requirements.txt',
|
|
17
|
+
buildOutput: 'dist',
|
|
18
|
+
hasDocker,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default { detect, getInfo };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} [cwd]
|
|
6
|
+
*/
|
|
7
|
+
function detect(cwd = process.cwd()) {
|
|
8
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
9
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
10
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
11
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
12
|
+
return !!(deps.react && !deps.next);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} [cwd]
|
|
17
|
+
*/
|
|
18
|
+
function getInfo(cwd = process.cwd()) {
|
|
19
|
+
const pkg = fs.readJsonSync(path.join(cwd, 'package.json'));
|
|
20
|
+
const scripts = pkg.scripts || {};
|
|
21
|
+
return {
|
|
22
|
+
framework: 'react',
|
|
23
|
+
buildCommand: scripts.build ? 'npm run build' : 'npm run build',
|
|
24
|
+
buildOutput: fs.existsSync(path.join(cwd, 'dist')) ? 'dist' : 'build',
|
|
25
|
+
hasDocker: fs.existsSync(path.join(cwd, 'Dockerfile')),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default { detect, getInfo };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
function detect(cwd = process.cwd()) {
|
|
5
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
6
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
7
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
8
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
9
|
+
return !!deps.vue;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getInfo(cwd = process.cwd()) {
|
|
13
|
+
const pkg = fs.readJsonSync(path.join(cwd, 'package.json'));
|
|
14
|
+
const scripts = pkg.scripts || {};
|
|
15
|
+
return {
|
|
16
|
+
framework: 'vue',
|
|
17
|
+
buildCommand: scripts.build ? 'npm run build' : 'npm run build',
|
|
18
|
+
buildOutput: fs.existsSync(path.join(cwd, 'dist')) ? 'dist' : 'build',
|
|
19
|
+
hasDocker: fs.existsSync(path.join(cwd, 'Dockerfile')),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default { detect, getInfo };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} stage
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
function timestamp() {
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const h = String(now.getHours()).padStart(2, '0');
|
|
10
|
+
const m = String(now.getMinutes()).padStart(2, '0');
|
|
11
|
+
const s = String(now.getSeconds()).padStart(2, '0');
|
|
12
|
+
return `${h}:${m}:${s}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} stage
|
|
17
|
+
* @param {string} message
|
|
18
|
+
* @param {'info'|'success'|'warn'|'error'} level
|
|
19
|
+
*/
|
|
20
|
+
function log(stage, message, level = 'info') {
|
|
21
|
+
const prefix = chalk.gray(`[${timestamp()}]`) + chalk.cyan(` [${stage}]`);
|
|
22
|
+
const colors = {
|
|
23
|
+
info: chalk.white,
|
|
24
|
+
success: chalk.green,
|
|
25
|
+
warn: chalk.yellow,
|
|
26
|
+
error: chalk.red,
|
|
27
|
+
};
|
|
28
|
+
console.log(`${prefix} ${colors[level](message)}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} stage
|
|
33
|
+
* @returns {{ info: Function, success: Function, warn: Function, error: Function }}
|
|
34
|
+
*/
|
|
35
|
+
export function createLogger(stage) {
|
|
36
|
+
return {
|
|
37
|
+
info: (msg) => log(stage, msg, 'info'),
|
|
38
|
+
success: (msg) => log(stage, msg, 'success'),
|
|
39
|
+
warn: (msg) => log(stage, msg, 'warn'),
|
|
40
|
+
error: (msg) => log(stage, msg, 'error'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default { createLogger };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
import { createLogger } from '../logger/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
6
|
+
* @param {{ success: boolean, version: string, message?: string, deployUrl?: string, environment?: string }} result
|
|
7
|
+
*/
|
|
8
|
+
export async function sendEmailNotification(config, result) {
|
|
9
|
+
const log = createLogger('email');
|
|
10
|
+
|
|
11
|
+
const host = process.env.SMTP_HOST;
|
|
12
|
+
const port = parseInt(process.env.SMTP_PORT || '587', 10);
|
|
13
|
+
const user = process.env.SMTP_USER;
|
|
14
|
+
const pass = process.env.SMTP_PASS;
|
|
15
|
+
const to = process.env.NOTIFY_EMAIL_TO || process.env.NOTIFICATION_EMAIL;
|
|
16
|
+
|
|
17
|
+
if (!host || !to) {
|
|
18
|
+
throw new Error('SMTP_HOST and NOTIFY_EMAIL_TO (or NOTIFICATION_EMAIL) must be set');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const transporter = nodemailer.createTransport({
|
|
22
|
+
host,
|
|
23
|
+
port,
|
|
24
|
+
secure: port === 465,
|
|
25
|
+
auth: user && pass ? { user, pass } : undefined,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const status = result.success ? 'SUCCESS' : 'FAILED';
|
|
29
|
+
const environment = result.environment || process.env.DEPLOYHUB_ENV || 'production';
|
|
30
|
+
const deployUrl = result.deployUrl || config.healthCheck?.url || 'N/A';
|
|
31
|
+
|
|
32
|
+
const subject = `[DeployHub] ${status}: ${config.project} v${result.version} (${environment})`;
|
|
33
|
+
const body = [
|
|
34
|
+
`Project: ${config.project}`,
|
|
35
|
+
`Version: ${result.version}`,
|
|
36
|
+
`Environment: ${environment}`,
|
|
37
|
+
`Status: ${status}`,
|
|
38
|
+
`Deploy URL: ${deployUrl}`,
|
|
39
|
+
'',
|
|
40
|
+
result.message || (result.success ? 'Deployment completed successfully.' : 'Deployment failed.'),
|
|
41
|
+
].join('\n');
|
|
42
|
+
|
|
43
|
+
await transporter.sendMail({
|
|
44
|
+
from: user || `deployhub@${host}`,
|
|
45
|
+
to,
|
|
46
|
+
subject,
|
|
47
|
+
text: body,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
log.success(`Email notification sent to ${to}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default { sendEmailNotification };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createLogger } from '../logger/index.js';
|
|
3
|
+
import { sendSlackNotification } from './slack.js';
|
|
4
|
+
import { sendEmailNotification } from './email.js';
|
|
5
|
+
import { sendWebhookNotification } from './webhook.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
9
|
+
* @param {{ success: boolean, version: string, message?: string, deployUrl?: string, environment?: string }} result
|
|
10
|
+
*/
|
|
11
|
+
export async function sendNotifications(config, result) {
|
|
12
|
+
const log = createLogger('notify');
|
|
13
|
+
const tasks = [];
|
|
14
|
+
|
|
15
|
+
if (config.notifications.slack) {
|
|
16
|
+
tasks.push(sendSlackNotification(result));
|
|
17
|
+
}
|
|
18
|
+
if (config.notifications.email) {
|
|
19
|
+
tasks.push(sendEmailNotification(config, result));
|
|
20
|
+
}
|
|
21
|
+
if (config.notifications.webhook) {
|
|
22
|
+
tasks.push(sendWebhookNotification(result));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (tasks.length === 0) {
|
|
26
|
+
log.info('No notifications configured');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await Promise.allSettled(tasks);
|
|
31
|
+
log.success('Notifications sent');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default { sendNotifications };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {{ success: boolean, version: string, message?: string }} result
|
|
5
|
+
*/
|
|
6
|
+
export async function sendSlackNotification(result) {
|
|
7
|
+
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
|
|
8
|
+
if (!webhookUrl) {
|
|
9
|
+
throw new Error('SLACK_WEBHOOK_URL not set');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const status = result.success ? '✅' : '❌';
|
|
13
|
+
await axios.post(webhookUrl, {
|
|
14
|
+
text: `${status} DeployHub: v${result.version} — ${result.message || (result.success ? 'Deployment succeeded' : 'Deployment failed')}`,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default { sendSlackNotification };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {{ success: boolean, version: string, message?: string }} result
|
|
5
|
+
*/
|
|
6
|
+
export async function sendWebhookNotification(result) {
|
|
7
|
+
const url = process.env.WEBHOOK_URL;
|
|
8
|
+
if (!url) {
|
|
9
|
+
throw new Error('WEBHOOK_URL not set');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
await axios.post(url, {
|
|
13
|
+
event: 'deployhub.deployment',
|
|
14
|
+
success: result.success,
|
|
15
|
+
version: result.version,
|
|
16
|
+
message: result.message,
|
|
17
|
+
timestamp: new Date().toISOString(),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default { sendWebhookNotification };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { downloadFromFirst } from '../storage/index.js';
|
|
2
|
+
import { createPlatformProvider } from '../deployment/providers/platforms/index.js';
|
|
3
|
+
import { extractArtifact } from '../artifact/engine.js';
|
|
4
|
+
import { createLogger } from '../logger/index.js';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
10
|
+
* @param {string} artifactDir
|
|
11
|
+
* @param {string} envName
|
|
12
|
+
*/
|
|
13
|
+
async function rollbackPlatformTarget(config, artifactDir, envName) {
|
|
14
|
+
const envConfig = config.environments[envName];
|
|
15
|
+
if (!envConfig) {
|
|
16
|
+
throw new Error(`Environment "${envName}" not found in config`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const deploymentPath = path.join(artifactDir, 'deployment.json');
|
|
20
|
+
if (await fs.pathExists(deploymentPath)) {
|
|
21
|
+
const data = await fs.readJson(deploymentPath);
|
|
22
|
+
const deployments = data.deployments || data.platformDeployments || [];
|
|
23
|
+
const record = deployments.find((d) => d.environmentName === envName) || data.lastDeployment;
|
|
24
|
+
if (record?.platform) {
|
|
25
|
+
const log = createLogger('rollback');
|
|
26
|
+
log.info(
|
|
27
|
+
`Rolling back ${envName} on ${record.platform} (deploy ${record.deployId || record.deploymentId || 'previous'})...`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (envConfig.deploymentType === 'platform' || envConfig.frontendDeploymentType === 'platform') {
|
|
33
|
+
const platform = envConfig.platform;
|
|
34
|
+
if (!platform) {
|
|
35
|
+
throw new Error(`No platform configured for environment "${envName}"`);
|
|
36
|
+
}
|
|
37
|
+
const provider = createPlatformProvider(platform, config, envName);
|
|
38
|
+
await provider.rollback(artifactDir);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
envConfig.frontendDeploymentType === 'platform' &&
|
|
44
|
+
(envConfig.backendDeploymentType === 'server' || envConfig.type)
|
|
45
|
+
) {
|
|
46
|
+
const platformProvider = createPlatformProvider(envConfig.platform, config, envName);
|
|
47
|
+
await platformProvider.rollback(artifactDir);
|
|
48
|
+
const { getDeploymentProvider } = await import('../deployment/index.js');
|
|
49
|
+
const serverProvider = getDeploymentProvider(envConfig.type, config, envName);
|
|
50
|
+
await serverProvider.rollback(artifactDir);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { getDeploymentProvider } = await import('../deployment/index.js');
|
|
55
|
+
const provider = getDeploymentProvider(envConfig.type, config, envName);
|
|
56
|
+
await provider.rollback(artifactDir);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
61
|
+
* @param {string} version
|
|
62
|
+
* @param {string} [cwd]
|
|
63
|
+
*/
|
|
64
|
+
export async function rollbackToVersion(config, version, cwd = process.cwd()) {
|
|
65
|
+
const log = createLogger('rollback');
|
|
66
|
+
const remoteKey = `${config.project}/v${version}/artifact.zip`;
|
|
67
|
+
const restoreDir = path.join(cwd, '.deployhub-restore', `v${version}`);
|
|
68
|
+
const artifactDir = path.join(restoreDir, 'artifact');
|
|
69
|
+
|
|
70
|
+
log.info(`Downloading artifact v${version}...`);
|
|
71
|
+
await fs.emptyDir(restoreDir);
|
|
72
|
+
await fs.ensureDir(artifactDir);
|
|
73
|
+
|
|
74
|
+
const zipPath = path.join(artifactDir, 'artifact.zip');
|
|
75
|
+
await downloadFromFirst(config.storage, remoteKey, zipPath);
|
|
76
|
+
|
|
77
|
+
log.info('Extracting artifact for rollback...');
|
|
78
|
+
const extractedDir = path.join(artifactDir, '_extracted');
|
|
79
|
+
await fs.emptyDir(extractedDir);
|
|
80
|
+
await extractArtifact(artifactDir, extractedDir);
|
|
81
|
+
|
|
82
|
+
const extractedDeployment = path.join(extractedDir, 'deployment.json');
|
|
83
|
+
if (await fs.pathExists(extractedDeployment)) {
|
|
84
|
+
await fs.copy(extractedDeployment, path.join(artifactDir, 'deployment.json'));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const targets = config.deploy || [];
|
|
88
|
+
if (targets.length === 0) {
|
|
89
|
+
log.warn('No deployment targets configured');
|
|
90
|
+
return artifactDir;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
log.info('Executing platform-specific rollback from deployment.json...');
|
|
94
|
+
for (const envName of targets) {
|
|
95
|
+
await rollbackPlatformTarget(config, artifactDir, envName);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
log.success(`Rollback to v${version} complete`);
|
|
99
|
+
return artifactDir;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default { rollbackToVersion };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { createLogger } from '../logger/index.js';
|
|
3
|
+
import { createAwsProvider } from './providers/aws.js';
|
|
4
|
+
import { createLocalProvider } from './providers/local.js';
|
|
5
|
+
import { createAzureProvider } from './providers/azure.js';
|
|
6
|
+
import { createGcpProvider } from './providers/gcp.js';
|
|
7
|
+
import { createGdriveProvider } from './providers/gdrive.js';
|
|
8
|
+
import { createDropboxProvider } from './providers/dropbox.js';
|
|
9
|
+
import { createFtpProvider } from './providers/ftp.js';
|
|
10
|
+
|
|
11
|
+
/** @type {Record<string, (env?: Record<string, string>) => import('./providers/aws.js').default>} */
|
|
12
|
+
const PROVIDER_FACTORIES = {
|
|
13
|
+
aws: createAwsProvider,
|
|
14
|
+
local: createLocalProvider,
|
|
15
|
+
azure: createAzureProvider,
|
|
16
|
+
gcp: createGcpProvider,
|
|
17
|
+
gdrive: createGdriveProvider,
|
|
18
|
+
dropbox: createDropboxProvider,
|
|
19
|
+
ftp: createFtpProvider,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} name
|
|
24
|
+
* @param {Record<string, string>} [env]
|
|
25
|
+
*/
|
|
26
|
+
export function getStorageProvider(name, env = process.env) {
|
|
27
|
+
const factory = PROVIDER_FACTORIES[name];
|
|
28
|
+
if (!factory) {
|
|
29
|
+
throw new Error(`Unknown storage provider: ${name}`);
|
|
30
|
+
}
|
|
31
|
+
return factory(env);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string[]} providers
|
|
36
|
+
* @param {string} zipPath
|
|
37
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
38
|
+
*/
|
|
39
|
+
export async function uploadToAll(providers, zipPath, config) {
|
|
40
|
+
const log = createLogger('storage');
|
|
41
|
+
const remoteKey = `${config.project}/v${config.version}/artifact.zip`;
|
|
42
|
+
|
|
43
|
+
const uploads = providers.map(async (name) => {
|
|
44
|
+
try {
|
|
45
|
+
const provider = getStorageProvider(name);
|
|
46
|
+
log.info(`Uploading to ${name}...`);
|
|
47
|
+
await provider.upload(zipPath, remoteKey);
|
|
48
|
+
log.success(`Uploaded to ${name}`);
|
|
49
|
+
return { name, success: true };
|
|
50
|
+
} catch (err) {
|
|
51
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
52
|
+
throw new Error(`Storage upload to ${name} failed: ${message}`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return Promise.all(uploads);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string[]} providers
|
|
61
|
+
* @param {string} remoteKey
|
|
62
|
+
* @param {string} localPath
|
|
63
|
+
*/
|
|
64
|
+
export async function downloadFromFirst(providers, remoteKey, localPath) {
|
|
65
|
+
for (const name of providers) {
|
|
66
|
+
const provider = getStorageProvider(name);
|
|
67
|
+
const exists = await provider.verify(remoteKey);
|
|
68
|
+
if (exists) {
|
|
69
|
+
await provider.download(remoteKey, localPath);
|
|
70
|
+
return name;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`Artifact not found in any configured storage provider`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} name
|
|
78
|
+
*/
|
|
79
|
+
export async function testProvider(name) {
|
|
80
|
+
const provider = getStorageProvider(name);
|
|
81
|
+
await provider.testConnection();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string[]} providers
|
|
86
|
+
*/
|
|
87
|
+
export async function testAllProviders(providers) {
|
|
88
|
+
const results = await Promise.allSettled(
|
|
89
|
+
providers.map(async (name) => {
|
|
90
|
+
await testProvider(name);
|
|
91
|
+
return { name, status: 'connected' };
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return results.map((result, i) => {
|
|
96
|
+
const name = providers[i];
|
|
97
|
+
if (result.status === 'fulfilled') {
|
|
98
|
+
return { name, status: 'connected' };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
name,
|
|
102
|
+
status: 'error',
|
|
103
|
+
error: result.reason?.message || String(result.reason),
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { PROVIDER_FACTORIES };
|
|
109
|
+
export default { getStorageProvider, uploadToAll, downloadFromFirst, testProvider, testAllProviders };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { S3Client, HeadBucketCommand, DeleteObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import { Upload } from '@aws-sdk/lib-storage';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {Record<string, string>} env
|
|
8
|
+
*/
|
|
9
|
+
export function createAwsProvider(env = process.env) {
|
|
10
|
+
const bucket = env.AWS_BUCKET;
|
|
11
|
+
const region = env.AWS_REGION || 'us-east-1';
|
|
12
|
+
|
|
13
|
+
if (!env.AWS_ACCESS_KEY_ID || !env.AWS_SECRET_ACCESS_KEY || !bucket) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'AWS credentials incomplete. Set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET in .env'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const client = new S3Client({
|
|
20
|
+
region,
|
|
21
|
+
credentials: {
|
|
22
|
+
accessKeyId: env.AWS_ACCESS_KEY_ID,
|
|
23
|
+
secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} localPath
|
|
29
|
+
* @param {string} [remoteKey]
|
|
30
|
+
*/
|
|
31
|
+
async function upload(localPath, remoteKey) {
|
|
32
|
+
const key =
|
|
33
|
+
remoteKey ||
|
|
34
|
+
path.basename(localPath).replace(/\\/g, '/');
|
|
35
|
+
|
|
36
|
+
const fileStream = fs.createReadStream(localPath);
|
|
37
|
+
const upload = new Upload({
|
|
38
|
+
client,
|
|
39
|
+
params: {
|
|
40
|
+
Bucket: bucket,
|
|
41
|
+
Key: key,
|
|
42
|
+
Body: fileStream,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
await upload.done();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} remoteKey
|
|
50
|
+
* @param {string} localPath
|
|
51
|
+
*/
|
|
52
|
+
async function download(remoteKey, localPath) {
|
|
53
|
+
const response = await client.send(
|
|
54
|
+
new GetObjectCommand({ Bucket: bucket, Key: remoteKey })
|
|
55
|
+
);
|
|
56
|
+
await fs.ensureDir(path.dirname(localPath));
|
|
57
|
+
const body = response.Body;
|
|
58
|
+
if (!body) throw new Error('Empty response from S3');
|
|
59
|
+
const chunks = [];
|
|
60
|
+
for await (const chunk of body) {
|
|
61
|
+
chunks.push(chunk);
|
|
62
|
+
}
|
|
63
|
+
await fs.writeFile(localPath, Buffer.concat(chunks));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} remoteKey
|
|
68
|
+
*/
|
|
69
|
+
async function verify(remoteKey) {
|
|
70
|
+
try {
|
|
71
|
+
await client.send(
|
|
72
|
+
new GetObjectCommand({ Bucket: bucket, Key: remoteKey })
|
|
73
|
+
);
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} remoteKey
|
|
82
|
+
*/
|
|
83
|
+
async function deleteObject(remoteKey) {
|
|
84
|
+
await client.send(
|
|
85
|
+
new DeleteObjectCommand({ Bucket: bucket, Key: remoteKey })
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function testConnection() {
|
|
90
|
+
await client.send(new HeadBucketCommand({ Bucket: bucket }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { upload, download, verify, delete: deleteObject, testConnection };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default { createAwsProvider };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { BlobServiceClient } from '@azure/storage-blob';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export function createAzureProvider(env = process.env) {
|
|
6
|
+
const connectionString = env.AZURE_CONNECTION_STRING;
|
|
7
|
+
const container = env.AZURE_CONTAINER;
|
|
8
|
+
|
|
9
|
+
if (!connectionString || !container) {
|
|
10
|
+
throw new Error('Azure credentials incomplete. Set AZURE_CONNECTION_STRING and AZURE_CONTAINER.');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const client = BlobServiceClient.fromConnectionString(connectionString);
|
|
14
|
+
const containerClient = client.getContainerClient(container);
|
|
15
|
+
|
|
16
|
+
async function upload(localPath, remoteKey) {
|
|
17
|
+
const key = remoteKey || path.basename(localPath);
|
|
18
|
+
const blockBlob = containerClient.getBlockBlobClient(key);
|
|
19
|
+
await blockBlob.uploadFile(localPath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function download(remoteKey, localPath) {
|
|
23
|
+
const blockBlob = containerClient.getBlockBlobClient(remoteKey);
|
|
24
|
+
await fs.ensureDir(path.dirname(localPath));
|
|
25
|
+
await blockBlob.downloadToFile(localPath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function verify(remoteKey) {
|
|
29
|
+
const blockBlob = containerClient.getBlockBlobClient(remoteKey);
|
|
30
|
+
return blockBlob.exists();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function deleteObject(remoteKey) {
|
|
34
|
+
const blockBlob = containerClient.getBlockBlobClient(remoteKey);
|
|
35
|
+
await blockBlob.deleteIfExists();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function testConnection() {
|
|
39
|
+
await containerClient.getProperties();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { upload, download, verify, delete: deleteObject, testConnection };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default { createAzureProvider };
|