@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,164 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import { createLogger } from '../../../logger/index.js';
|
|
5
|
+
import {
|
|
6
|
+
runCli,
|
|
7
|
+
saveDeploymentRecord,
|
|
8
|
+
readPreviousPlatformDeployment,
|
|
9
|
+
checkUrlHealth,
|
|
10
|
+
} from './_shared.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {import('../../../core/config.js').DeployHubConfig} config
|
|
14
|
+
* @param {string} envName
|
|
15
|
+
* @param {Record<string, string>} [env]
|
|
16
|
+
*/
|
|
17
|
+
export function createAwsAmplifyProvider(config, envName, env = process.env) {
|
|
18
|
+
const environment = config.environments[envName];
|
|
19
|
+
const log = createLogger('aws-amplify');
|
|
20
|
+
const appId = environment?.appId || env.AMPLIFY_APP_ID;
|
|
21
|
+
const region = environment?.region || env.AWS_REGION || 'us-east-1';
|
|
22
|
+
const githubConnected = environment?.githubConnected ?? false;
|
|
23
|
+
|
|
24
|
+
async function deploy(artifactDir) {
|
|
25
|
+
if (!appId) throw new Error('AMPLIFY_APP_ID is required');
|
|
26
|
+
|
|
27
|
+
const awsEnv = {
|
|
28
|
+
AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID || '',
|
|
29
|
+
AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY || '',
|
|
30
|
+
AWS_DEFAULT_REGION: region,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (githubConnected) {
|
|
34
|
+
log.info('Triggering Amplify release job (GitHub connected)...');
|
|
35
|
+
const result = await runCli(
|
|
36
|
+
`aws amplify start-job --app-id ${appId} --branch-name main --job-type RELEASE`,
|
|
37
|
+
process.cwd(),
|
|
38
|
+
awsEnv
|
|
39
|
+
);
|
|
40
|
+
if (result.exitCode !== 0) {
|
|
41
|
+
throw new Error(`Amplify start-job failed: ${result.stderr || result.stdout}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const jobMatch = (result.stdout || '').match(/"jobId":\s*"([^"]+)"/);
|
|
45
|
+
const jobId = jobMatch?.[1] || '';
|
|
46
|
+
|
|
47
|
+
await saveDeploymentRecord(
|
|
48
|
+
artifactDir,
|
|
49
|
+
{
|
|
50
|
+
platform: 'aws-amplify',
|
|
51
|
+
deploymentId: jobId,
|
|
52
|
+
appId,
|
|
53
|
+
method: 'github',
|
|
54
|
+
},
|
|
55
|
+
envName
|
|
56
|
+
);
|
|
57
|
+
log.success(`Amplify job started: ${jobId || 'RELEASE'}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
log.info('Uploading artifact zip to Amplify...');
|
|
62
|
+
const zipPath = path.join(artifactDir, 'artifact.zip');
|
|
63
|
+
if (!(await fs.pathExists(zipPath))) {
|
|
64
|
+
throw new Error('artifact.zip not found for Amplify upload');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const createResult = await runCli(
|
|
68
|
+
`aws amplify create-deployment --app-id ${appId} --branch-name main --output json`,
|
|
69
|
+
process.cwd(),
|
|
70
|
+
awsEnv
|
|
71
|
+
);
|
|
72
|
+
if (createResult.exitCode !== 0) {
|
|
73
|
+
throw new Error(`Amplify create-deployment failed: ${createResult.stderr}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const deployment = JSON.parse(createResult.stdout);
|
|
77
|
+
const jobId = deployment.jobId;
|
|
78
|
+
const zipUploadUrl = deployment.zipUploadUrl;
|
|
79
|
+
|
|
80
|
+
await execa('curl', ['-T', zipPath, zipUploadUrl], {
|
|
81
|
+
env: { ...process.env, ...awsEnv },
|
|
82
|
+
stdio: 'inherit',
|
|
83
|
+
shell: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const startResult = await runCli(
|
|
87
|
+
`aws amplify start-deployment --app-id ${appId} --branch-name main --job-id ${jobId}`,
|
|
88
|
+
process.cwd(),
|
|
89
|
+
awsEnv
|
|
90
|
+
);
|
|
91
|
+
if (startResult.exitCode !== 0) {
|
|
92
|
+
throw new Error(`Amplify start-deployment failed: ${startResult.stderr}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await saveDeploymentRecord(
|
|
96
|
+
artifactDir,
|
|
97
|
+
{
|
|
98
|
+
platform: 'aws-amplify',
|
|
99
|
+
deploymentId: jobId,
|
|
100
|
+
appId,
|
|
101
|
+
method: 'zip',
|
|
102
|
+
},
|
|
103
|
+
envName
|
|
104
|
+
);
|
|
105
|
+
log.success(`Amplify deployment started: job ${jobId}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function rollback(artifactDir) {
|
|
109
|
+
if (!appId) throw new Error('AMPLIFY_APP_ID is required');
|
|
110
|
+
|
|
111
|
+
const previous = await readPreviousPlatformDeployment(artifactDir);
|
|
112
|
+
const jobId = previous?.deploymentId;
|
|
113
|
+
if (!jobId) {
|
|
114
|
+
throw new Error('No previous Amplify job ID found for rollback');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const awsEnv = {
|
|
118
|
+
AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID || '',
|
|
119
|
+
AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY || '',
|
|
120
|
+
AWS_DEFAULT_REGION: region,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
log.info(`Re-triggering Amplify job ${jobId}...`);
|
|
124
|
+
const result = await runCli(
|
|
125
|
+
`aws amplify start-job --app-id ${appId} --branch-name main --job-id ${jobId} --job-type RETRY`,
|
|
126
|
+
process.cwd(),
|
|
127
|
+
awsEnv
|
|
128
|
+
);
|
|
129
|
+
if (result.exitCode !== 0) {
|
|
130
|
+
await runCli(
|
|
131
|
+
`aws amplify start-job --app-id ${appId} --branch-name main --job-type RELEASE`,
|
|
132
|
+
process.cwd(),
|
|
133
|
+
awsEnv
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
log.success('Amplify rollback triggered');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function healthCheck() {
|
|
140
|
+
const url = config.healthCheck?.url;
|
|
141
|
+
if (!url) return true;
|
|
142
|
+
return checkUrlHealth(url);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function testConnection() {
|
|
146
|
+
if (!appId) throw new Error('AMPLIFY_APP_ID is required');
|
|
147
|
+
const result = await runCli(
|
|
148
|
+
`aws amplify get-app --app-id ${appId}`,
|
|
149
|
+
process.cwd(),
|
|
150
|
+
{
|
|
151
|
+
AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID || '',
|
|
152
|
+
AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY || '',
|
|
153
|
+
AWS_DEFAULT_REGION: region,
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
if (result.exitCode !== 0) {
|
|
157
|
+
throw new Error('Could not find Amplify app — check AMPLIFY_APP_ID and AWS credentials');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { deploy, rollback, healthCheck, testConnection };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default { createAwsAmplifyProvider };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createLogger } from '../../../logger/index.js';
|
|
2
|
+
import {
|
|
3
|
+
runCli,
|
|
4
|
+
saveDeploymentRecord,
|
|
5
|
+
readPreviousPlatformDeployment,
|
|
6
|
+
checkUrlHealth,
|
|
7
|
+
resolveBuildOutputPath,
|
|
8
|
+
} from './_shared.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {import('../../../core/config.js').DeployHubConfig} config
|
|
12
|
+
* @param {string} envName
|
|
13
|
+
* @param {Record<string, string>} [env]
|
|
14
|
+
*/
|
|
15
|
+
export function createAzureStaticWebAppsProvider(config, envName, env = process.env) {
|
|
16
|
+
const environment = config.environments[envName];
|
|
17
|
+
const log = createLogger('azure-swa');
|
|
18
|
+
const token = env.AZURE_STATIC_WEB_APPS_TOKEN;
|
|
19
|
+
const resourceName = environment?.resourceName || config.project;
|
|
20
|
+
|
|
21
|
+
async function deploy(artifactDir) {
|
|
22
|
+
if (!token) throw new Error('AZURE_STATIC_WEB_APPS_TOKEN is required');
|
|
23
|
+
|
|
24
|
+
const buildDir = resolveBuildOutputPath(config);
|
|
25
|
+
log.info(`Deploying to Azure Static Web Apps (${resourceName})...`);
|
|
26
|
+
|
|
27
|
+
const result = await runCli(
|
|
28
|
+
`swa deploy "${buildDir}" --deployment-token=${token}`,
|
|
29
|
+
process.cwd()
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (result.exitCode !== 0) {
|
|
33
|
+
throw new Error(`Azure SWA deploy failed: ${result.stderr || result.stdout}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await saveDeploymentRecord(
|
|
37
|
+
artifactDir,
|
|
38
|
+
{
|
|
39
|
+
platform: 'azure-static-web-apps',
|
|
40
|
+
deploymentId: `swa-${Date.now()}`,
|
|
41
|
+
resourceName,
|
|
42
|
+
deploymentUrl: config.healthCheck?.url || '',
|
|
43
|
+
},
|
|
44
|
+
envName
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
log.success('Deployed to Azure Static Web Apps');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function rollback(artifactDir) {
|
|
51
|
+
log.info('Azure SWA has no native rollback — re-deploying previous artifact...');
|
|
52
|
+
await deploy(artifactDir);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function healthCheck() {
|
|
56
|
+
const url = config.healthCheck?.url;
|
|
57
|
+
if (!url) return true;
|
|
58
|
+
return checkUrlHealth(url);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function testConnection() {
|
|
62
|
+
if (!token) throw new Error('AZURE_STATIC_WEB_APPS_TOKEN is required');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { deploy, rollback, healthCheck, testConnection };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default { createAzureStaticWebAppsProvider };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createLogger } from '../../../logger/index.js';
|
|
3
|
+
import {
|
|
4
|
+
runCli,
|
|
5
|
+
saveDeploymentRecord,
|
|
6
|
+
readPreviousPlatformDeployment,
|
|
7
|
+
checkUrlHealth,
|
|
8
|
+
resolveBuildOutputPath,
|
|
9
|
+
} from './_shared.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('../../../core/config.js').DeployHubConfig} config
|
|
13
|
+
* @param {string} envName
|
|
14
|
+
* @param {Record<string, string>} [env]
|
|
15
|
+
*/
|
|
16
|
+
export function createCloudflarePagesProvider(config, envName, env = process.env) {
|
|
17
|
+
const environment = config.environments[envName];
|
|
18
|
+
const log = createLogger('cloudflare-pages');
|
|
19
|
+
const apiToken = env.CLOUDFLARE_API_TOKEN;
|
|
20
|
+
const accountId = environment?.accountId || env.CLOUDFLARE_ACCOUNT_ID;
|
|
21
|
+
const projectName =
|
|
22
|
+
environment?.projectName || env.CF_PROJECT_NAME || config.project;
|
|
23
|
+
|
|
24
|
+
async function deploy(artifactDir) {
|
|
25
|
+
if (!apiToken || !accountId) {
|
|
26
|
+
throw new Error('CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are required');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const buildDir = resolveBuildOutputPath(config);
|
|
30
|
+
log.info(`Deploying to Cloudflare Pages project "${projectName}"...`);
|
|
31
|
+
|
|
32
|
+
const result = await runCli(
|
|
33
|
+
`wrangler pages deploy "${buildDir}" --project-name=${projectName} --branch=main`,
|
|
34
|
+
process.cwd(),
|
|
35
|
+
{
|
|
36
|
+
CLOUDFLARE_API_TOKEN: apiToken,
|
|
37
|
+
CLOUDFLARE_ACCOUNT_ID: accountId,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (result.exitCode !== 0) {
|
|
42
|
+
throw new Error(`Cloudflare Pages deploy failed: ${result.stderr || result.stdout}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const urlMatch = (result.stdout || '').match(/https:\/\/[^\s]+\.pages\.dev/);
|
|
46
|
+
const deploymentUrl = urlMatch?.[0] || config.healthCheck?.url || '';
|
|
47
|
+
|
|
48
|
+
await saveDeploymentRecord(
|
|
49
|
+
artifactDir,
|
|
50
|
+
{
|
|
51
|
+
platform: 'cloudflare-pages',
|
|
52
|
+
deploymentUrl,
|
|
53
|
+
deploymentId: deploymentUrl,
|
|
54
|
+
projectName,
|
|
55
|
+
accountId,
|
|
56
|
+
},
|
|
57
|
+
envName
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
log.success(`Deployed to Cloudflare Pages: ${deploymentUrl || projectName}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function rollback(artifactDir) {
|
|
64
|
+
if (!apiToken || !accountId) {
|
|
65
|
+
throw new Error('CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are required');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const previous = await readPreviousPlatformDeployment(artifactDir);
|
|
69
|
+
const deploymentId = previous?.deploymentId;
|
|
70
|
+
if (!deploymentId) {
|
|
71
|
+
throw new Error('No previous Cloudflare deployment ID found for rollback');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log.info('Rolling back Cloudflare Pages deployment...');
|
|
75
|
+
await axios.post(
|
|
76
|
+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/deployments/${deploymentId}/rollback`,
|
|
77
|
+
{},
|
|
78
|
+
{ headers: { Authorization: `Bearer ${apiToken}` } }
|
|
79
|
+
);
|
|
80
|
+
log.success('Cloudflare Pages rollback complete');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function healthCheck() {
|
|
84
|
+
const url = config.healthCheck?.url;
|
|
85
|
+
if (!url) return true;
|
|
86
|
+
return checkUrlHealth(url);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function testConnection() {
|
|
90
|
+
if (!apiToken) throw new Error('CLOUDFLARE_API_TOKEN is required');
|
|
91
|
+
const response = await axios.get(
|
|
92
|
+
'https://api.cloudflare.com/client/v4/user/tokens/verify',
|
|
93
|
+
{ headers: { Authorization: `Bearer ${apiToken}` } }
|
|
94
|
+
);
|
|
95
|
+
if (!response.data?.success) {
|
|
96
|
+
throw new Error('Invalid CLOUDFLARE_API_TOKEN');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { deploy, rollback, healthCheck, testConnection };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default { createCloudflarePagesProvider };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createLogger } from '../../../logger/index.js';
|
|
2
|
+
import {
|
|
3
|
+
runCli,
|
|
4
|
+
saveDeploymentRecord,
|
|
5
|
+
checkUrlHealth,
|
|
6
|
+
} from './_shared.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {import('../../../core/config.js').DeployHubConfig} config
|
|
10
|
+
* @param {string} envName
|
|
11
|
+
* @param {Record<string, string>} [env]
|
|
12
|
+
*/
|
|
13
|
+
export function createFirebaseAppHostingProvider(config, envName, env = process.env) {
|
|
14
|
+
const environment = config.environments[envName];
|
|
15
|
+
const log = createLogger('firebase-app-hosting');
|
|
16
|
+
const token = env.FIREBASE_TOKEN;
|
|
17
|
+
const projectId = environment?.projectId || env.FIREBASE_PROJECT_ID;
|
|
18
|
+
const backendName =
|
|
19
|
+
environment?.backendName || env.FIREBASE_APP_HOSTING_BACKEND || config.project;
|
|
20
|
+
|
|
21
|
+
async function deploy(artifactDir) {
|
|
22
|
+
if (!token || !projectId) {
|
|
23
|
+
throw new Error('FIREBASE_TOKEN and FIREBASE_PROJECT_ID are required');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
log.info(`Deploying to Firebase App Hosting (${projectId}/${backendName})...`);
|
|
27
|
+
|
|
28
|
+
const createResult = await runCli(
|
|
29
|
+
`firebase apphosting:backends:list --project=${projectId} --token=${token} --non-interactive`,
|
|
30
|
+
process.cwd()
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const backendExists =
|
|
34
|
+
createResult.exitCode === 0 && createResult.stdout.includes(backendName);
|
|
35
|
+
|
|
36
|
+
if (!backendExists) {
|
|
37
|
+
log.info(`Creating App Hosting backend "${backendName}"...`);
|
|
38
|
+
await runCli(
|
|
39
|
+
`firebase apphosting:backends:create --project=${projectId} --token=${token} --backend=${backendName} --non-interactive`,
|
|
40
|
+
process.cwd()
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await runCli(
|
|
45
|
+
`firebase deploy --only apphosting --token=${token} --project=${projectId} --non-interactive`,
|
|
46
|
+
process.cwd()
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (result.exitCode !== 0) {
|
|
50
|
+
throw new Error(`Firebase App Hosting deploy failed: ${result.stderr || result.stdout}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await saveDeploymentRecord(
|
|
54
|
+
artifactDir,
|
|
55
|
+
{
|
|
56
|
+
platform: 'firebase-app-hosting',
|
|
57
|
+
deploymentId: `apphosting-${Date.now()}`,
|
|
58
|
+
projectId,
|
|
59
|
+
backendName,
|
|
60
|
+
deploymentUrl: config.healthCheck?.url || '',
|
|
61
|
+
},
|
|
62
|
+
envName
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
log.success('Deployed to Firebase App Hosting');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function rollback(artifactDir) {
|
|
69
|
+
log.info('Firebase App Hosting rollback — re-deploying previous artifact...');
|
|
70
|
+
await deploy(artifactDir);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function healthCheck() {
|
|
74
|
+
const url = config.healthCheck?.url;
|
|
75
|
+
if (!url) return true;
|
|
76
|
+
return checkUrlHealth(url);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function testConnection() {
|
|
80
|
+
if (!token || !projectId) {
|
|
81
|
+
throw new Error('FIREBASE_TOKEN and FIREBASE_PROJECT_ID are required');
|
|
82
|
+
}
|
|
83
|
+
const result = await runCli(
|
|
84
|
+
`firebase projects:list --token=${token} --non-interactive`,
|
|
85
|
+
process.cwd()
|
|
86
|
+
);
|
|
87
|
+
if (result.exitCode !== 0) {
|
|
88
|
+
throw new Error('Invalid FIREBASE_TOKEN');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { deploy, rollback, healthCheck, testConnection };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default { createFirebaseAppHostingProvider };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createLogger } from '../../../logger/index.js';
|
|
2
|
+
import { ensureFirebaseJson } from '../../../utils/firebase-config-generator.js';
|
|
3
|
+
import {
|
|
4
|
+
runCli,
|
|
5
|
+
saveDeploymentRecord,
|
|
6
|
+
readPreviousPlatformDeployment,
|
|
7
|
+
checkUrlHealth,
|
|
8
|
+
getBuildOutputDir,
|
|
9
|
+
} from './_shared.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('../../../core/config.js').DeployHubConfig} config
|
|
13
|
+
* @param {string} envName
|
|
14
|
+
* @param {Record<string, string>} [env]
|
|
15
|
+
*/
|
|
16
|
+
export function createFirebaseHostingProvider(config, envName, env = process.env) {
|
|
17
|
+
const environment = config.environments[envName];
|
|
18
|
+
const log = createLogger('firebase-hosting');
|
|
19
|
+
const token = env.FIREBASE_TOKEN;
|
|
20
|
+
const projectId = environment?.projectId || env.FIREBASE_PROJECT_ID;
|
|
21
|
+
|
|
22
|
+
async function deploy(artifactDir) {
|
|
23
|
+
if (!token || !projectId) {
|
|
24
|
+
throw new Error('FIREBASE_TOKEN and FIREBASE_PROJECT_ID are required');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
const buildOutput = getBuildOutputDir(config);
|
|
29
|
+
await ensureFirebaseJson(buildOutput, cwd);
|
|
30
|
+
|
|
31
|
+
log.info(`Deploying to Firebase Hosting (${projectId})...`);
|
|
32
|
+
const result = await runCli(
|
|
33
|
+
`firebase deploy --only hosting --token=${token} --project=${projectId} --non-interactive`,
|
|
34
|
+
cwd
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (result.exitCode !== 0) {
|
|
38
|
+
throw new Error(`Firebase deploy failed: ${result.stderr || result.stdout}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const urlMatch = (result.stdout || '').match(/Hosting URL:\s*(https:\/\/\S+)/i);
|
|
42
|
+
const deploymentUrl = urlMatch?.[1] || config.healthCheck?.url || '';
|
|
43
|
+
|
|
44
|
+
await saveDeploymentRecord(
|
|
45
|
+
artifactDir,
|
|
46
|
+
{
|
|
47
|
+
platform: 'firebase-hosting',
|
|
48
|
+
deploymentUrl,
|
|
49
|
+
deploymentId: deploymentUrl,
|
|
50
|
+
projectId,
|
|
51
|
+
},
|
|
52
|
+
envName
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
log.success(`Deployed to Firebase Hosting: ${deploymentUrl || projectId}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function rollback(artifactDir) {
|
|
59
|
+
if (!token || !projectId) {
|
|
60
|
+
throw new Error('FIREBASE_TOKEN and FIREBASE_PROJECT_ID are required');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
log.info('Rolling back Firebase Hosting...');
|
|
64
|
+
const result = await runCli(
|
|
65
|
+
`firebase hosting:rollback --token=${token} --project=${projectId} --non-interactive`,
|
|
66
|
+
process.cwd()
|
|
67
|
+
);
|
|
68
|
+
if (result.exitCode !== 0) {
|
|
69
|
+
throw new Error(`Firebase rollback failed: ${result.stderr || result.stdout}`);
|
|
70
|
+
}
|
|
71
|
+
log.success('Firebase Hosting rollback complete');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function healthCheck() {
|
|
75
|
+
const url = config.healthCheck?.url;
|
|
76
|
+
if (!url) return true;
|
|
77
|
+
return checkUrlHealth(url);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function testConnection() {
|
|
81
|
+
if (!token || !projectId) {
|
|
82
|
+
throw new Error('FIREBASE_TOKEN and FIREBASE_PROJECT_ID are required');
|
|
83
|
+
}
|
|
84
|
+
const result = await runCli(
|
|
85
|
+
`firebase projects:list --token=${token} --non-interactive`,
|
|
86
|
+
process.cwd()
|
|
87
|
+
);
|
|
88
|
+
if (result.exitCode !== 0) {
|
|
89
|
+
throw new Error('Invalid FIREBASE_TOKEN');
|
|
90
|
+
}
|
|
91
|
+
if (!result.stdout.includes(projectId)) {
|
|
92
|
+
throw new Error(`Firebase project "${projectId}" not accessible`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { deploy, rollback, healthCheck, testConnection };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default { createFirebaseHostingProvider };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createVercelProvider } from './vercel.js';
|
|
2
|
+
import { createNetlifyProvider } from './netlify.js';
|
|
3
|
+
import { createCloudflarePagesProvider } from './cloudflare-pages.js';
|
|
4
|
+
import { createAwsAmplifyProvider } from './aws-amplify.js';
|
|
5
|
+
import { createAzureStaticWebAppsProvider } from './azure-static-web-apps.js';
|
|
6
|
+
import { createFirebaseHostingProvider } from './firebase-hosting.js';
|
|
7
|
+
import { createFirebaseAppHostingProvider } from './firebase-app-hosting.js';
|
|
8
|
+
|
|
9
|
+
/** @type {Record<string, Function>} */
|
|
10
|
+
const PLATFORM_FACTORIES = {
|
|
11
|
+
vercel: createVercelProvider,
|
|
12
|
+
netlify: createNetlifyProvider,
|
|
13
|
+
'cloudflare-pages': createCloudflarePagesProvider,
|
|
14
|
+
'aws-amplify': createAwsAmplifyProvider,
|
|
15
|
+
'azure-static-web-apps': createAzureStaticWebAppsProvider,
|
|
16
|
+
'firebase-hosting': createFirebaseHostingProvider,
|
|
17
|
+
'firebase-app-hosting': createFirebaseAppHostingProvider,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} platform
|
|
22
|
+
* @param {import('../../../core/config.js').DeployHubConfig} config
|
|
23
|
+
* @param {string} envName
|
|
24
|
+
* @param {Record<string, string>} [env]
|
|
25
|
+
*/
|
|
26
|
+
export function createPlatformProvider(platform, config, envName, env = process.env) {
|
|
27
|
+
const factory = PLATFORM_FACTORIES[platform];
|
|
28
|
+
if (!factory) {
|
|
29
|
+
throw new Error(`Unknown platform: ${platform}`);
|
|
30
|
+
}
|
|
31
|
+
return factory(config, envName, env);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
createVercelProvider,
|
|
36
|
+
createNetlifyProvider,
|
|
37
|
+
createCloudflarePagesProvider,
|
|
38
|
+
createAwsAmplifyProvider,
|
|
39
|
+
createAzureStaticWebAppsProvider,
|
|
40
|
+
createFirebaseHostingProvider,
|
|
41
|
+
createFirebaseAppHostingProvider,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default { createPlatformProvider, PLATFORM_FACTORIES };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createLogger } from '../../../logger/index.js';
|
|
3
|
+
import {
|
|
4
|
+
runCli,
|
|
5
|
+
saveDeploymentRecord,
|
|
6
|
+
readPreviousPlatformDeployment,
|
|
7
|
+
checkUrlHealth,
|
|
8
|
+
resolveBuildOutputPath,
|
|
9
|
+
} from './_shared.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('../../../core/config.js').DeployHubConfig} config
|
|
13
|
+
* @param {string} envName
|
|
14
|
+
* @param {Record<string, string>} [env]
|
|
15
|
+
*/
|
|
16
|
+
export function createNetlifyProvider(config, envName, env = process.env) {
|
|
17
|
+
const environment = config.environments[envName];
|
|
18
|
+
const log = createLogger('netlify');
|
|
19
|
+
const authToken = env.NETLIFY_AUTH_TOKEN;
|
|
20
|
+
const siteId = environment?.siteId || env.NETLIFY_SITE_ID;
|
|
21
|
+
|
|
22
|
+
async function deploy(artifactDir) {
|
|
23
|
+
if (!authToken || !siteId) {
|
|
24
|
+
throw new Error('NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID are required');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const buildDir = resolveBuildOutputPath(config);
|
|
28
|
+
log.info(`Deploying to Netlify from ${buildDir}...`);
|
|
29
|
+
|
|
30
|
+
const result = await runCli(
|
|
31
|
+
`netlify deploy --prod --dir="${buildDir}" --auth=${authToken} --site=${siteId}`,
|
|
32
|
+
process.cwd()
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (result.exitCode !== 0) {
|
|
36
|
+
throw new Error(`Netlify deploy failed: ${result.stderr || result.stdout}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const deployIdMatch = (result.stdout || '').match(/Deploy ID:\s*(\S+)/i);
|
|
40
|
+
const urlMatch = (result.stdout || '').match(/Website URL:\s*(https:\/\/\S+)/i);
|
|
41
|
+
const deployId = deployIdMatch?.[1] || '';
|
|
42
|
+
const deploymentUrl = urlMatch?.[1] || config.healthCheck?.url || '';
|
|
43
|
+
|
|
44
|
+
await saveDeploymentRecord(
|
|
45
|
+
artifactDir,
|
|
46
|
+
{
|
|
47
|
+
platform: 'netlify',
|
|
48
|
+
deploymentUrl,
|
|
49
|
+
deploymentId: deployId,
|
|
50
|
+
siteId,
|
|
51
|
+
},
|
|
52
|
+
envName
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
log.success(`Deployed to Netlify: ${deploymentUrl || siteId}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function rollback(artifactDir) {
|
|
59
|
+
if (!authToken || !siteId) {
|
|
60
|
+
throw new Error('NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID are required');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const previous = await readPreviousPlatformDeployment(artifactDir);
|
|
64
|
+
const deployId = previous?.deploymentId;
|
|
65
|
+
if (!deployId) {
|
|
66
|
+
throw new Error('No previous Netlify deployment ID found for rollback');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
log.info(`Restoring Netlify deploy ${deployId}...`);
|
|
70
|
+
await axios.post(
|
|
71
|
+
`https://api.netlify.com/api/v1/sites/${siteId}/deploys/${deployId}/restore`,
|
|
72
|
+
{},
|
|
73
|
+
{ headers: { Authorization: `Bearer ${authToken}` } }
|
|
74
|
+
);
|
|
75
|
+
log.success('Netlify rollback complete');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function healthCheck() {
|
|
79
|
+
const url = config.healthCheck?.url;
|
|
80
|
+
if (!url) return true;
|
|
81
|
+
return checkUrlHealth(url);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function testConnection() {
|
|
85
|
+
if (!authToken) throw new Error('NETLIFY_AUTH_TOKEN is required');
|
|
86
|
+
const response = await axios.get('https://api.netlify.com/api/v1/user', {
|
|
87
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
88
|
+
});
|
|
89
|
+
if (!response.data?.email) {
|
|
90
|
+
throw new Error('Invalid NETLIFY_AUTH_TOKEN');
|
|
91
|
+
}
|
|
92
|
+
if (siteId) {
|
|
93
|
+
await axios.get(`https://api.netlify.com/api/v1/sites/${siteId}`, {
|
|
94
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { deploy, rollback, healthCheck, testConnection };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default { createNetlifyProvider };
|